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

实现

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

1
2
3
4
5
6
7
8
//需要切换数据源
CustomerContextHolder.setCustomerType(CustomerContextHolder.DATASOURCE_B);

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!--统一的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

1
2
3
4
5
6
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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来进行数据库操作。

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

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

1
2
3
dataSource1 ---> sessionFactory1 --->
---> dao层实现
dataSource2 ---> sessionFactory2 --->

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

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

1
2
3
dataSource1 ---> 
---> dynamicDataSource ---> sessionFactory --> dao层实现
dataSource2 --->

SpringAbstractRoutingDataSource就是采用这种架构。

AbstractRoutingDataSource 的设计源码:

1
2
3
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean{
……
}

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

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

1
2
3
4
5
6
public Connection getConnection() throws SQLException {       
return determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
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(),这样就可以实现数据源的动态切换。

1
2
3
4
5
6
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return CustomerContextHolder.getCustomerType();
}
}