有些web应用需要连接多个数据源,本文讲解一下如何使用多个数据源,大题思路是这样的,系统启动的时候创建多个数据源,然后具体执行sql的时候去切换数据源执行对应的sql。如何切换数据源呢?spring提供了一个AbstractRoutingDataSource抽象类,只要继承这个类就可以了,这个类需要设置多个数据源,每个数据源有一个key对应,继承这个类必须实现determineCurrentLookupKey()方法,这个方法返回一个Object值,这个值应该是数据源的key,执行sql的时候会调用这个方法 获取key,然后根据这个key获取到的数据源执行sql。下面看具体的例子。
前面说了determineCurrentLookupKey()方法的返回值决定,选择什么数据库,方法执行的时候如何动态设置返回值呢?为了不影响其他线程的使用,使用线程本地变量是最好的,这样只会影响当前线程。看如下类
public class DataSourceRouter {
//默认数据源的key
public final static String DEFAULT = "default";
//数据源1的key
public final static String KEY_ONE = "key_one";
private final static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> DEFAULT);
/**
* 获取线程本地变量
*
* @return
*/
static String getCurrentDataSourceKey() {
return threadLocal.get();
}
/**
* @param key 数据源对应的key
* @param supplier sql执行的方法
* @param <T>
* @return sql执行的结果
*/
public static <T> T doWithKey(String key, Supplier<T> supplier) {
threadLocal.set(key);
T t = supplier.get();
threadLocal.set(DEFAULT);
return t;
}
}
这里通过doWithKey执行sql,通过determineCurrentLookupKey动态获取当前数据源。
下面来实现AbstractRoutingDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
ApplicationContext applicationContext;
/**
* 是否在创建数据源时,立即初始化连接
*/
private boolean initConnectionsOnCreate = false;
/**
* 返回配置的数据源的key,这样下面执行的sql就是使用该数据源的
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceRouter.getCurrentDataSourceKey();
}
public void init() {
DruidDataSource defaultTargetDataSource = applicationContext.getBean("defaultDataSource", DruidDataSource.class);
DruidDataSource dataSource1 = applicationContext.getBean("dataSourceTemplate", DruidDataSource.class);
dataSource1.setUrl("jdbc:mysql://localhost:3306/social?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
dataSource1.setUsername("root");
dataSource1.setPassword("998973");
if (initConnectionsOnCreate) {
try {
defaultTargetDataSource.init();
dataSource1.init();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceRouter.DEFAULT, defaultTargetDataSource);
map.put(DataSourceRouter.KEY_ONE, dataSource1);
setTargetDataSources(map);
//默认先执行配置文件中的targetDataSources属性的数据源,这里设置的数据源不会生效,必须调用afterPropertiesSet
afterPropertiesSet();
}
public void setInitConnectionsOnCreate(boolean initConnectionsOnCreate) {
this.initConnectionsOnCreate = initConnectionsOnCreate;
}
}
这里说明了,配置文件中配置的targetDataSources是必配的,对象创建的时候会读取这个配置然后调用afterPropertiesSet加载数据源,因为我们这里的数据源可能通过数据库等其他途径获取,所以没有写在配置文件中,这里需要手动调用一下afterPropertiesSet重新设置一下数据源。
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 数据源模板,动态增加数据源时需要用到,scope是prototype,非单例对象 -->
<bean id="dataSourceTemplate" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="filters" value="stat"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="maxOpenPreparedStatements" value="20"/>
<property name="asyncInit" value="true"/>
</bean>
<bean id="defaultDataSource" parent="dataSourceTemplate">
<property name="url"
value="jdbc:mysql://localhost:3306/easytour?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="998973"/>
</bean>
<!--统一的dataSource-->
<bean id="dynamicDataSource" class="com.zhan.design.config.datasource.DynamicDataSource" init-method="init">
<!-- 设置true时,随便一个key,找不到就走默认数据源,很可能带来不好的效果 -->
<property name="lenientFallback" value="false"/>
<property name="targetDataSources">
<map></map>
</property>
<!--设置默认的dataSource-->
<property name="defaultTargetDataSource" ref="defaultDataSource">
</property>
<!--是否在创建数据源时,立即初始化连接-->
<property name="initConnectionsOnCreate" value="true"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<!--扫描mybatis配置文件,在哪里可以做细配置-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描映射文件所在目录-->
<property name="mapperLocations" value="classpath:com/zhan/design/mapper/**/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--扫描接口的基础包,会把该包下面的所有接口注册为spring的bean-->
<property name="basePackage" value="com.zhan.design.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!--配置spring的事务-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource"/>
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
这样就完成了
下面看下具体使用的例子
@GetMapping("/test")
public Map<String, String> test() {
//通过调用doWithKey就完成对数据源的切换了
String label1 = DataSourceRouter.doWithKey(DataSourceRouter.DEFAULT, () -> dictionaryService.getByTypeAndKey("1", "1"));
String label2 = DataSourceRouter.doWithKey(DataSourceRouter.KEY_ONE, () -> dictionaryService.getByTypeAndKey("1", "1"));
Map<String, String> map = new HashMap<>();
map.put("default_label", label1);
map.put("one_label", label2);
return map;
}
至此就完成了。