实现动态切换多数据源及其原理分析

在开发中可能会遇到多个库的连接,那么一个库就是一个数据源,在程序中如何快速动态地切换数据源呢?本文来探讨一下spring提供的AbstractRoutingDataSource实现方案。

实现

比如我有三个数据源,分别交DATASOURCE_ADATASOURCE_BDATASOURCE_C,我假设默认是DATASOURCE_A,此时我需要用B来查询,我理想的效果是:

//需要切换数据源
CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_B);

List<xxx> xxxList = xxxService.getList();
System.out.println("====xxxList:"+xxxList.size());

//执行以后需要清除,否则后续的请求是继续在切换后的数据源中
CustomerContextHolder.clearCustomerType();

当我想用C的时候,直接一样的套路,也就是说只需要两行代码就可以实现数据源的自由切换,如何达到这种效果呢?

首先,数据源的定义肯定是要有的,我在xml中定义三个数据源,即dataSource

<!--统一的dataSource -->
<bean id="dynamicDataSource" class="com.xxx.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!--通过不同的key决定用哪个dataSource -->
            <entry value-ref="dataSource_A" key="dataSource_A"></entry>
            <entry value-ref="dataSource_B" key="dataSource_B"></entry>
            <entry value-ref="dataSource_C" key="dataSource_C"></entry>
        </map>
    </property>
    <!--设置默认的dataSource -->
    <property name="defaultTargetDataSource" ref="dataSource_A"></property>
</bean>

<!-- 1. 数据源 : dataSource_A -->
<bean id="dataSource_A" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://ip:port/db_A?useUnicode=true&amp;characterEncoding=utf-8" />
    <property name="username" value="xxxx" />
    <property name="password" value="xxxx" />
    <!-- 初始化连接大小 -->
    <property name="initialSize" value="5"></property>
    <!-- 连接池最大数量 -->
    <property name="maxActive" value="120"></property>
    <!-- 连接池最大空闲 -->
    <property name="maxIdle" value="30"></property>
    <!-- 连接池最小空闲 -->
    <property name="minIdle" value="10"></property>
    <!-- 获取连接最大等待时间 -->
    <property name="maxWait" value="60000"></property>
    <property name="validationQuery" value="SELECT 1" />
    <property name="testOnBorrow" value="true"/>
</bean>

<!-- 2. 数据源 : dataSource_B -->
<bean id="dataSource_B" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://ip:port/db_B?useUnicode=true&amp;characterEncoding=utf-8" />
    <property name="username" value="xxxx" />
    <property name="password" value="xxxx" />
    <!-- 初始化连接大小 -->
    <property name="initialSize" value="5"></property>
    <!-- 连接池最大数量 -->
    <property name="maxActive" value="120"></property>
    <!-- 连接池最大空闲 -->
    <property name="maxIdle" value="30"></property>
    <!-- 连接池最小空闲 -->
    <property name="minIdle" value="10"></property>
    <!-- 获取连接最大等待时间 -->
    <property name="maxWait" value="60000"></property>
    <property name="validationQuery" value="SELECT 1" />
    <property name="testOnBorrow" value="true"/>
</bean>


<!-- 3. 数据源 : dataSource_B -->
<bean id="dataSource_C" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://ip:port/db_C?useUnicode=true&amp;characterEncoding=utf-8" />
    <property name="username" value="xxxx" />
    <property name="password" value="xxxx" />
    <!-- 初始化连接大小 -->
    <property name="initialSize" value="5"></property>
    <!-- 连接池最大数量 -->
    <property name="maxActive" value="120"></property>
    <!-- 连接池最大空闲 -->
    <property name="maxIdle" value="30"></property>
    <!-- 连接池最小空闲 -->
    <property name="minIdle" value="10"></property>
    <!-- 获取连接最大等待时间 -->
    <property name="maxWait" value="60000"></property>
    <property name="validationQuery" value="SELECT 1" />
    <property name="testOnBorrow" value="true"/>
</bean>


<!--注入dynamicDataSource即动态数据源-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dynamicDataSource"></property>

    ....
</bean>

其他的配置文件全部略。此时我需要新建一个类去继承AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return CustomerContextHolder.getCustomerType();
    }

}

那么,我就可以根据这个返回值即key来找到对应的数据源。这里用到了ThreadLocal

public class CustomerContextHolder {

    public static final String DATA_SOURCE_A = "dataSource_A";
    public static final String DATA_SOURCE_B = "dataSource_B";
    public static final String DATA_SOURCE_C = "dataSource_C";
    // 用ThreadLocal来设置当前线程使用哪个dataSource
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    //设置数据源
    public static void setCustomerType(String customerType) {
        System.out.println("=========切换数据源:"+customerType);
        contextHolder.set(customerType);
    }

    public static String getCustomerType() {

        String dataSource = contextHolder.get();
        if (StringUtils.isEmpty(dataSource)) {
            //默认数据源
            return DATA_SOURCE_A;
        } else {
            return dataSource;
        }
    }

    //清除数据源,防止内存泄漏
    public static void clearCustomerType() {
        contextHolder.remove();
    }
}

至此,动态在多数据源中切换功能完成。问题是我为什么重写了determineCurrentLookupKey()就可以切换数据源了呢?

原理

多数据源还是比较头疼的,因为我们自己玩往往都是一个数据源,比如springmybatis结合的项目,我们在spring配置中往往是配置一个dataSource来连接数据库,然后绑定给sessionFactory,在dao层代码中再指定sessionFactory来进行数据库操作。

dataSource--->sessionFactory--->dao层实现类

这是单数据源dataSource结构,但是缺点很明显,不支持多个数据源,于是我们再改进一下,让它支持多数据源。

dataSource1 ---> sessionFactory1 --->
                                     ---> dao层实现
dataSource2 ---> sessionFactory2 --->

这种结构实现了多数据源,但是缺点也很明显,具有多个SessionFactory,不具有灵活性,而且太笨重了。如果再加一个数据源,就需要再加一个SessionFactory

顾名思义,SessionFactory,就是用来创建session会话的工厂。如果存在多个Sessionfactory 那么Session是不是就乱套了,因此这种架构不可取。那么下面这种架构就应用而生。

dataSource1 ---> 
                ---> dynamicDataSource ---> sessionFactory --> dao层实现
dataSource2 ---> 

SpringAbstractRoutingDataSource就是采用这种架构。

AbstractRoutingDataSource 的设计源码:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean{
    ……
}

扩展SpringAbstractRoutingDataSource抽象类(该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。)

从上可以看出它继承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子类吗,So我们可以分析下它的getConnection方法:

public Connection getConnection() throws SQLException {       
    return determineTargetDataSource().getConnection();   
}   
public Connection getConnection(String username, String password)  throws SQLException {        
    return determineTargetDataSource().getConnection(username, password);   
}

获取连接的方法中,重点是determineTargetDataSource方法,看源码:

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    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 + "]");
    }
    return dataSource;
}

上面这段源码的重点在于determineCurrentLookupKey()方法,这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSourcekey值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。

因此我们需要重写AbstractRoutingDataSource类的抽象方法determineCurrentLookupKey(),这样就可以实现数据源的动态切换。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return CustomerContextHolder.getCustomerType();
    }

}
全部评论

相关推荐

牛客868257804号:九个中铁八个中建
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务