在开发中可能会遇到多个库的连接,那么一个库就是一个数据源,在程序中如何快速动态地切换数据源呢?本文来探讨一下spring提供的AbstractRoutingDataSource实现方案。
实现
比如我有三个数据源,分别交DATASOURCE_A
、DATASOURCE_B
、DATASOURCE_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
| <bean id="dynamicDataSource" class="com.xxx.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <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> <property name="defaultTargetDataSource" ref="dataSource_A"></property> </bean>
<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&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>
<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&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>
<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&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>
<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"; 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()
就可以切换数据源了呢?
原理
多数据源还是比较头疼的,因为我们自己玩往往都是一个数据源,比如spring
和mybatis
结合的项目,我们在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 --->
|
Spring
的AbstractRoutingDataSource
就是采用这种架构。
AbstractRoutingDataSource
的设计源码:
1 2 3
| public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean{ …… }
|
扩展Spring
的AbstractRoutingDataSource
抽象类(该类充当了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
类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource
的key
值,有了这个key
值,resolvedDataSource
(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource
,如果找不到,就用配置默认的数据源。
因此我们需要重写AbstractRoutingDataSource
类的抽象方法determineCurrentLookupKey()
,这样就可以实现数据源的动态切换。
1 2 3 4 5 6
| public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return CustomerContextHolder.getCustomerType(); } }
|