前面章节讲解了 如何在 MyBatis + Druid 自定义多数据源,前面是静态配置,就是有几个数据源就配置几个 MyBatis 的配置,本章会将前面的第二个数据源配置改成动态数据源,也就是会在 2 个数据源之间进行按需切换,同一套 mapper 可以在这两个数据源上运行,它们的表结构是一致的。
其实这个动态数据源的核心原理就是:在获取数据库连接前,会有一个动作是 获取当前的数据源,那么获取当前数据源这个操作其实是有一个术语叫做 路由
路由数据源核心原理
spring jdbc 提供了一个扩展数据源 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 它可以实现选择指定的数据源产生连接
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {....@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return determineTargetDataSource().getConnection(username, password);}protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");// 获取当前要使用的路由数据源的 keyObject lookupKey = determineCurrentLookupKey();// 然后从 resolvedDataSources 中获取真正的数据源对象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;}/*** Determine the current lookup key. This will typically be* implemented to check a thread-bound transaction context.* <p>Allows for arbitrary keys. The returned key needs* to match the stored lookup key type, as resolved by the* {@link #resolveSpecifiedLookupKey} method.*/@Nullableprotected abstract Object determineCurrentLookupKey();}
可以看到,在获取 数据库连接前,会调用 determineTargetDataSource()获取要路由的数据源对象,最后会调用 determineCurrentLookupKey() 方法去返回一个 lookupKey,然后在准备好的真正的数据源中获取与之对应的 lookupKey 的数据源。
动态路由的实现
这里先实现 AbstractRoutingDataSource 实现
package cn.mrcode.autoconfig.mybatis;import lombok.Getter;import lombok.Setter;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/*** 路由数据源*/public class Db02DataSourceRouter extends AbstractRoutingDataSource {@Getter@Setterprivate volatile String currentKey = "ds1";@Overrideprotected Object determineCurrentLookupKey() {// 返回当前的数据源 key/*一般的动态数据源做法会使用拦截器,去查找 mapper 上的自定义注解写的是数据源名称,然后使用 ThreadLocal 方式,设置获取到的 key在这里从 ThreadLocal 中获取返回在切面中执行完目标方法之后,再从 ThreadLocal 中清除掉而我这里的方式采用统一按需进行切换,所以只需要在该类成员上定义当前使用哪一个数据源即可可以按自己的业务场景触发变更该成员变量的值来达到切换数据源的目的*/return currentKey;}}
然后改造 MyBatis 数据源的配置代码
package cn.mrcode.autoconfig.mybatis.mls;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;import org.mybatis.spring.SqlSessionFactoryBean;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.core.io.support.ResourcePatternResolver;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import tk.mybatis.spring.annotation.MapperScan;import javax.sql.DataSource;import java.io.IOException;import java.util.HashMap;import java.util.Map;@Configuration@MapperScan(value = {"cn.mrcode.repo.mapper.db02"},sqlSessionFactoryRef = "db02SqlSessionFactoryBean")public class MlsMyBatisConfigurer {@Bean("db0201DataSource")@ConfigurationProperties(prefix = "spring.datasource.db0201")public DataSource dataSource1() {return DruidDataSourceBuilder.create().build();}@Bean("db0202DataSource")@ConfigurationProperties(prefix = "spring.datasource.db0202")public DataSource dataSource2() {return DruidDataSourceBuilder.create().build();}/*** 目标数据源集合,方便在路由里面选择** @param ds1* @param ds2* @return*/@Bean("db02TargetDataSources")public Map<Object, Object> targetDataSources(@Qualifier("mlsDb1DataSource") DataSource ds1,@Qualifier("mlsDb2DataSource") DataSource ds2) {HashMap<Object, Object> map = new HashMap<>();map.put("ds1", ds1);map.put("ds2", ds2);return map;}@Bean("db02DataSource")public DataSource dataSource(@Qualifier("db02TargetDataSources") Map<Object, Object> targetDataSources) {Db02DataSourceRouter sourceRouter = new Db02DataSourceRouter();sourceRouter.setTargetDataSources(targetDataSources);// 该方法会被 bean ioc 容器调用// 同时,如果更改了 targetDataSources 里面的内容,也可以手动调用该方法使 router 里面的相关成员得到更新// sourceRouter.afterPropertiesSet();return sourceRouter;}/*** 配置 mybatis*/@Bean("db02SqlSessionFactoryBean")public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db02DataSource") DataSource dataSource) throws IOException {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dataSource);// 这里需要一个 Resource 可变数组,如何写?/* 其实这个可以通过查看他的自动配置源码是如何写的mybatis:mapper-locations: /mapper/*.xml*/ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("/mapper/db02/**/*.xml"));return sqlSessionFactoryBean;}@Bean("db02DataSourceTransactionManager")public DataSourceTransactionManager transactionManager(@Qualifier("db02DataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}}
可以看到,上面改动的有以下几点:
- 将原来直接对应到数据库中的数据源,这里说的是 SqlSessionFactoryBean 中需要的数据源,替换成了 Db02DataSourceRouter 这个路由类
- 就是构建 Db02DataSourceRouter 这个类了,里面需要对应数据库的普通数据源
下面来看看 yaml 中的配置变成了什么样子
spring:datasource:druid:# 让 druid 的自动配置生效,配置监控相关功能# 配置 DruidStatFilterweb-stat-filter:enabled: trueurl-pattern: "/*"exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"# 配置DruidStatViewServletstat-view-servlet:enabled: trueurl-pattern: "/druid/*"# IP白名单(没有配置或者为空,则允许所有访问)# allow: 127.0.0.1,192.168.163.1allow: ""# IP黑名单 (存在共同时,deny优先于allow)# deny: 192.168.1.73# 禁用HTML页面上的“Reset All”功能reset-enable: false# 登录名 和 密码login-username: adminlogin-password: 123456# 多数据源配置db01:name: DB-01url: jdbc:mysql://127.0.0.1:3307/test1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=trueusername: rootpassword: root# 连接池的配置信息# 初始化大小,最小,最大initialSize: 5minIdle: 5maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: false# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,slf4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000# 多数据源配置db0201:name: DB-02-01url: jdbc:mysql://127.0.0.1:3307/test2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=trueusername: rootpassword: root# 连接池的配置信息# 初始化大小,最小,最大initialSize: 5minIdle: 5maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: false# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,slf4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000# 多数据源配置db0202:name: DB-02-02url: jdbc:mysql://127.0.0.1:3307/test3?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=trueusername: rootpassword: root# 连接池的配置信息# 初始化大小,最小,最大initialSize: 5minIdle: 5maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: false# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,slf4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
测试
这里的测试就比较简单了,比如可以写一个 controller,只要改变 Db02DataSourceRouter 中的 currentKey 参数即可
@Autowired@Qualifier("db02DataSource")private DataSource db02DataSourceRouter;@ApiOperation("数据源配置切换测试")@PostMapping("ds-switch")public Result dsSwitch(int index) {Db02DataSourceRouter router = (Db02DataSourceRouter) mlsDataSourceRouter;// 设置当前生效的数据源router.setCurrentKey(index == 1 ? "ds1" : "ds2");return ResultHelper.ok(router.getCurrentKey());}
遗留的问题
配置打印 myabtis 相关的日志信息
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
在获取链接的时候能看到如下 debug 的日志信息
was not registered for synchronization because synchronization is not activeCreating a new SqlSession
以上报错信息是在 org.mybatis.spring.SqlSessionUtils#registerSessionHolder 中 debug 信息,暂时不明白具体是什么原因导致的,但是不影响功能的使用。后续有时间,或则有谁知道的请给我留言。
