Spring + Mybatis 多数据源配置与使用总结

前段时间接手一个新的项目,需要同时涉及到两个数据库。两个数据库相关性较小,各自独立,其中一个数据库只需要获取相关业务注册信息。通过查找资料,找到了两种解决问题的办法。

1. spring配置文件配置2个数据源

配置文件如下所示:

<!-- 使用alibaba的Druid数据库连接池 -->
<!-- 配置数据源One -->
<bean id="dataSourceOne" class="com.alibaba.druid.pool.DruidDataSource"
      init-method="init" destroy-method="close">
    <property name="url" value="${url_one}"/>
    <property name="username" value="${username_one}"/>
    <property name="password" value="${password_one}"/>
    <property name="removeAbandoned" value="true"/>
    <property name="removeAbandonedTimeout" value="180"/>
    <property name="timeBetweenEvictionRunsMillis" value="20000"/>
    <property name="minEvictableIdleTimeMillis" value="30000"/>
    <property name="maxWait" value="5000"/>
    <property name="testWhileIdle" value="true"/>
    <property name="maxActive" value="200"/>
    <property name="minIdle" value="25"/>
    <property name="initialSize" value="25"/>
    <property name="validationQueryTimeout" value="50"/>
    <property name="validationQuery" value="SELECT 'x'" />
</bean>

   <bean id="sqlSessionFactoryOne" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSourceOne" />
    <property name="configLocation" value="classpath:conf/mybatis-config-one.xml" />
</bean>

<!-- mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 扫描包路径,如果需要扫描多个包,中间使用半角逗号隔开 -->
    <property name="basePackage" value="com.test.storage.dao.mapper.one" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryOne"/>
</bean>


<!-- 配置数据源Two -->
<bean id="dataSourceTwo" class="com.alibaba.druid.pool.DruidDataSource"
      init-method="init" destroy-method="close">
    <property name="url" value="${url_two}"/>
    <property name="username" value="${username_two}"/>
    <property name="password" value="${password_two}"/>
    <property name="removeAbandoned" value="true"/>
    <property name="removeAbandonedTimeout" value="180"/>
    <property name="timeBetweenEvictionRunsMillis" value="20000"/>
    <property name="minEvictableIdleTimeMillis" value="30000"/>
    <property name="maxWait" value="5000"/>
    <property name="testWhileIdle" value="true"/>
    <property name="maxActive" value="200"/>
    <property name="minIdle" value="25"/>
    <property name="initialSize" value="25"/>
    <property name="validationQueryTimeout" value="50"/>
    <property name="validationQuery" value="SELECT 'x'" />
</bean>

<bean id="sqlSessionFactoryTwo" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSourceTwo" />
    <property name="configLocation" value="classpath:conf/mybatis-config-two.xml" />
</bean>

<!-- mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!-- 扫描包路径,如果需要扫描多个包,中间使用半角逗号隔开 -->
    <property name="basePackage" value="com.test.storage.dao.mapper.two" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryTwo"/>
</bean>

   

此种方法中除了 dataSource,sqlSessionFactory 和 MapperScannerConfigurer 都是配置了2份,mybatis-config.xml也是配置了2份,需要将不同数据源中使用到的别名和mapper.xml文件分开配置的。 此种方法的好处是mapper.xml 与 数据源绑定在了一起,业务层对底层是无感知的,缺点是当涉及到多个数据源的操作时是无法做到事务的。

2. 基于 AOP 的多数据源的配置

此方法涉及到了AbstractRoutingDataSource 抽象类。 需要自定义一个继承AbstractRoutingDataSource的DataSource类MultipleDataSource。 然后通过 AOP 动态的选择数据源的切换。

首先我们定义一个多数据源的枚举,如下所示:

public enum MultiDataSourceTypeEnum {

    One("dataSourceOne"),
    Two("dataSourceTwo") ;

    private String name;

    MultiDataSourceTypeEnum(String name) {
        this.name = name;
}

然后定义一个多数据源的管理类,通过 ThreadLocal 来标识每次使用的数据源。

public class MultiDataSourceTypeManager {

    private static final ThreadLocal<MultiDataSourceTypeEnum> dataSourceTypeEnum = new ThreadLocal<MultiDataSourceTypeEnum>(){
        @Override
        protected MultiDataSourceTypeEnum initialValue(){
            return MultiDataSourceTypeEnum.One;
        }
    };

    public static MultiDataSourceTypeEnum get(){
        return dataSourceTypeEnum .get();
    }

    public static void set(MultiDataSourceTypeEnum dataSourceType){
        dataSourceTypeEnum .set(dataSourceType);
    }    

}

接下来就是我们继承 AbstractRoutingDataSource 的自定义 DataSource类了。通过determineCurrentLookupKey 方法返回每次需要使用的数据源。

public class MultiDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return MultiDataSourceTypeManager.get();
    }
}

在配置文件中,除了如第一种方法一样定义两个数据源,还需要将我们自定义的 MultiDataSource 配置如下:

<bean id="dataSource" class="com.test.multiDatasource.MultiDataSource">
    <property name="defaultTargetDataSource" ref="dataSourceOne" />
    <property name="targetDataSources">
        <map key-type="com.test.multiDatasource.MultiDataSourceTypeEnum">
            <entry key="One" value-ref="dataSourceOne"/>
            <entry key="Two" value-ref="dataSourceTwo"/>
        </map>
    </property>
</bean>

下面就是我们具体控制数据源切换的地方。定义了两个切点,实现在service层调用mapper的时候将数据源进行切换。我是默认使用数据源one作为主数据源。

@Aspect
@Component
public class MultiDataSourceInterceptor {

    public static final Logger logger = Logger.getLogger(MultiDataSourceInterceptor.class);

    @Pointcut("execution(* com.test.service.two..*.*(..))")
    public void dataSourceTwo(){};

    @Pointcut("execution(* com.test.service.one..*.*(..))")
    public void dataSourceOne(){};

    @Before("dataSourceTwo()")
    public void beforeTwo(JoinPoint jp) {
        MultiDataSourceTypeManager.set(MultiDataSourceTypeEnum.Two);
        logger.info("MultiDataSourceInterceptor:" + MultiDataSourceTypeEnum.Two.toString());
    }

    @After("dataSourceTwo()")
    public void afterTwo(JoinPoint jp) {
        MultiDataSourceTypeManager.set(MultiDataSourceTypeEnum.One);
        logger.info("MultiDataSourceInterceptor:" + MultiDataSourceTypeEnum.One.toString());
    }

    @Before("dataSourceOne()")
    public void beforeOne(JoinPoint jp) {
        MultiDataSourceTypeManager.set(MultiDataSourceTypeEnum.One);
        logger.info("MultiDataSourceInterceptor:" + MultiDataSourceTypeEnum.One.toString());
    }

}

此方法扩展十分容易,切换的粒度通过AOP可以很好的控制。

原理分析:

我们自定义的 MultiDataSource 是继承了 AbstractRoutingDataSource的。在实例化bean的时候会调用 afterPropertiesSet()函数,该函数如下:

public void afterPropertiesSet() {
    if(this.targetDataSources == null) {
        throw new IllegalArgumentException("Property 'targetDataSources' is required");
    } else {
        this.resolvedDataSources = new HashMap(this.targetDataSources.size());
        Iterator var2 = this.targetDataSources.entrySet().iterator();

        while(var2.hasNext()) {
            Entry entry = (Entry)var2.next();
            Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }

        if(this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }

    }
}

注意这里的this.targetDataSources就是我们在配置文件中配置的

    <property name="targetDataSources">
        <map key-type="com.test.multiDatasource.MultiDataSourceTypeEnum">
            <entry key="One" value-ref="dataSourceOne"/>
            <entry key="Two" value-ref="dataSourceTwo"/>
        </map>
    </property>

通过此处,就将我们自定义的两个数据源定义进来了。在选择具体的数据源时会调用determineTargetDataSource()函数,该函数如下:

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if(dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }

    if(dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}

注意此处的 this.determineCurrentLookupKey()函数,我们在 MultiDataSource 中重写了

protected Object determineCurrentLookupKey() {
    return MultiDataSourceTypeManager.get();
}

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者杜满意授权发布。