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