前面章节讲解了 如何在 MyBatis + Druid 自定义多数据源,前面是静态配置,就是有几个数据源就配置几个 MyBatis 的配置,本章会将前面的第二个数据源配置改成动态数据源,也就是会在 2 个数据源之间进行按需切换,同一套 mapper 可以在这两个数据源上运行,它们的表结构是一致的。

其实这个动态数据源的核心原理就是:在获取数据库连接前,会有一个动作是 获取当前的数据源,那么获取当前数据源这个操作其实是有一个术语叫做 路由

路由数据源核心原理

spring jdbc 提供了一个扩展数据源 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 它可以实现选择指定的数据源产生连接

  1. public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  2. ....
  3. @Override
  4. public Connection getConnection() throws SQLException {
  5. return determineTargetDataSource().getConnection();
  6. }
  7. @Override
  8. public Connection getConnection(String username, String password) throws SQLException {
  9. return determineTargetDataSource().getConnection(username, password);
  10. }
  11. protected DataSource determineTargetDataSource() {
  12. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  13. // 获取当前要使用的路由数据源的 key
  14. Object lookupKey = determineCurrentLookupKey();
  15. // 然后从 resolvedDataSources 中获取真正的数据源对象
  16. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
  17. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  18. dataSource = this.resolvedDefaultDataSource;
  19. }
  20. if (dataSource == null) {
  21. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  22. }
  23. return dataSource;
  24. }
  25. /**
  26. * Determine the current lookup key. This will typically be
  27. * implemented to check a thread-bound transaction context.
  28. * <p>Allows for arbitrary keys. The returned key needs
  29. * to match the stored lookup key type, as resolved by the
  30. * {@link #resolveSpecifiedLookupKey} method.
  31. */
  32. @Nullable
  33. protected abstract Object determineCurrentLookupKey();
  34. }

可以看到,在获取 数据库连接前,会调用 determineTargetDataSource()获取要路由的数据源对象,最后会调用 determineCurrentLookupKey() 方法去返回一个 lookupKey,然后在准备好的真正的数据源中获取与之对应的 lookupKey 的数据源。

动态路由的实现

这里先实现 AbstractRoutingDataSource 实现

  1. package cn.mrcode.autoconfig.mybatis;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  5. /**
  6. * 路由数据源
  7. */
  8. public class Db02DataSourceRouter extends AbstractRoutingDataSource {
  9. @Getter
  10. @Setter
  11. private volatile String currentKey = "ds1";
  12. @Override
  13. protected Object determineCurrentLookupKey() {
  14. // 返回当前的数据源 key
  15. /*
  16. 一般的动态数据源做法会使用拦截器,去查找 mapper 上的自定义注解写的是数据源名称,
  17. 然后使用 ThreadLocal 方式,设置获取到的 key
  18. 在这里从 ThreadLocal 中获取返回
  19. 在切面中执行完目标方法之后,再从 ThreadLocal 中清除掉
  20. 而我这里的方式采用统一按需进行切换,所以只需要在该类成员上定义当前使用哪一个数据源即可
  21. 可以按自己的业务场景触发变更该成员变量的值来达到切换数据源的目的
  22. */
  23. return currentKey;
  24. }
  25. }

然后改造 MyBatis 数据源的配置代码

  1. package cn.mrcode.autoconfig.mybatis.mls;
  2. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  3. import org.mybatis.spring.SqlSessionFactoryBean;
  4. import org.springframework.beans.factory.annotation.Qualifier;
  5. import org.springframework.boot.context.properties.ConfigurationProperties;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
  9. import org.springframework.core.io.support.ResourcePatternResolver;
  10. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
  11. import tk.mybatis.spring.annotation.MapperScan;
  12. import javax.sql.DataSource;
  13. import java.io.IOException;
  14. import java.util.HashMap;
  15. import java.util.Map;
  16. @Configuration
  17. @MapperScan(
  18. value = {
  19. "cn.mrcode.repo.mapper.db02"
  20. },
  21. sqlSessionFactoryRef = "db02SqlSessionFactoryBean")
  22. public class MlsMyBatisConfigurer {
  23. @Bean("db0201DataSource")
  24. @ConfigurationProperties(prefix = "spring.datasource.db0201")
  25. public DataSource dataSource1() {
  26. return DruidDataSourceBuilder.create().build();
  27. }
  28. @Bean("db0202DataSource")
  29. @ConfigurationProperties(prefix = "spring.datasource.db0202")
  30. public DataSource dataSource2() {
  31. return DruidDataSourceBuilder.create().build();
  32. }
  33. /**
  34. * 目标数据源集合,方便在路由里面选择
  35. *
  36. * @param ds1
  37. * @param ds2
  38. * @return
  39. */
  40. @Bean("db02TargetDataSources")
  41. public Map<Object, Object> targetDataSources(
  42. @Qualifier("mlsDb1DataSource") DataSource ds1,
  43. @Qualifier("mlsDb2DataSource") DataSource ds2
  44. ) {
  45. HashMap<Object, Object> map = new HashMap<>();
  46. map.put("ds1", ds1);
  47. map.put("ds2", ds2);
  48. return map;
  49. }
  50. @Bean("db02DataSource")
  51. public DataSource dataSource(@Qualifier("db02TargetDataSources") Map<Object, Object> targetDataSources) {
  52. Db02DataSourceRouter sourceRouter = new Db02DataSourceRouter();
  53. sourceRouter.setTargetDataSources(targetDataSources);
  54. // 该方法会被 bean ioc 容器调用
  55. // 同时,如果更改了 targetDataSources 里面的内容,也可以手动调用该方法使 router 里面的相关成员得到更新
  56. // sourceRouter.afterPropertiesSet();
  57. return sourceRouter;
  58. }
  59. /**
  60. * 配置 mybatis
  61. */
  62. @Bean("db02SqlSessionFactoryBean")
  63. public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db02DataSource") DataSource dataSource) throws IOException {
  64. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  65. sqlSessionFactoryBean.setDataSource(dataSource);
  66. // 这里需要一个 Resource 可变数组,如何写?
  67. /* 其实这个可以通过查看他的自动配置源码是如何写的
  68. mybatis:
  69. mapper-locations: /mapper/*.xml
  70. */
  71. ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
  72. sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("/mapper/db02/**/*.xml"));
  73. return sqlSessionFactoryBean;
  74. }
  75. @Bean("db02DataSourceTransactionManager")
  76. public DataSourceTransactionManager transactionManager(@Qualifier("db02DataSource") DataSource dataSource) {
  77. return new DataSourceTransactionManager(dataSource);
  78. }
  79. }

可以看到,上面改动的有以下几点:

  1. 将原来直接对应到数据库中的数据源,这里说的是 SqlSessionFactoryBean 中需要的数据源,替换成了 Db02DataSourceRouter 这个路由类
  2. 就是构建 Db02DataSourceRouter 这个类了,里面需要对应数据库的普通数据源

下面来看看 yaml 中的配置变成了什么样子

  1. spring:
  2. datasource:
  3. druid:
  4. # 让 druid 的自动配置生效,配置监控相关功能
  5. # 配置 DruidStatFilter
  6. web-stat-filter:
  7. enabled: true
  8. url-pattern: "/*"
  9. exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
  10. # 配置DruidStatViewServlet
  11. stat-view-servlet:
  12. enabled: true
  13. url-pattern: "/druid/*"
  14. # IP白名单(没有配置或者为空,则允许所有访问)
  15. # allow: 127.0.0.1,192.168.163.1
  16. allow: ""
  17. # IP黑名单 (存在共同时,deny优先于allow)
  18. # deny: 192.168.1.73
  19. # 禁用HTML页面上的“Reset All”功能
  20. reset-enable: false
  21. # 登录名 和 密码
  22. login-username: admin
  23. login-password: 123456
  24. # 多数据源配置
  25. db01:
  26. name: DB-01
  27. url: jdbc:mysql://127.0.0.1:3307/test1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
  28. username: root
  29. password: root
  30. # 连接池的配置信息
  31. # 初始化大小,最小,最大
  32. initialSize: 5
  33. minIdle: 5
  34. maxActive: 20
  35. # 配置获取连接等待超时的时间
  36. maxWait: 60000
  37. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  38. timeBetweenEvictionRunsMillis: 60000
  39. # 配置一个连接在池中最小生存的时间,单位是毫秒
  40. minEvictableIdleTimeMillis: 300000
  41. validationQuery: SELECT 1 FROM DUAL
  42. testWhileIdle: true
  43. testOnBorrow: false
  44. testOnReturn: false
  45. # 打开PSCache,并且指定每个连接上PSCache的大小
  46. poolPreparedStatements: true
  47. maxPoolPreparedStatementPerConnectionSize: 20
  48. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  49. filters: stat,wall,slf4j
  50. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  51. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  52. # 多数据源配置
  53. db0201:
  54. name: DB-02-01
  55. url: jdbc:mysql://127.0.0.1:3307/test2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
  56. username: root
  57. password: root
  58. # 连接池的配置信息
  59. # 初始化大小,最小,最大
  60. initialSize: 5
  61. minIdle: 5
  62. maxActive: 20
  63. # 配置获取连接等待超时的时间
  64. maxWait: 60000
  65. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  66. timeBetweenEvictionRunsMillis: 60000
  67. # 配置一个连接在池中最小生存的时间,单位是毫秒
  68. minEvictableIdleTimeMillis: 300000
  69. validationQuery: SELECT 1 FROM DUAL
  70. testWhileIdle: true
  71. testOnBorrow: false
  72. testOnReturn: false
  73. # 打开PSCache,并且指定每个连接上PSCache的大小
  74. poolPreparedStatements: true
  75. maxPoolPreparedStatementPerConnectionSize: 20
  76. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  77. filters: stat,wall,slf4j
  78. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  79. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  80. # 多数据源配置
  81. db0202:
  82. name: DB-02-02
  83. url: jdbc:mysql://127.0.0.1:3307/test3?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
  84. username: root
  85. password: root
  86. # 连接池的配置信息
  87. # 初始化大小,最小,最大
  88. initialSize: 5
  89. minIdle: 5
  90. maxActive: 20
  91. # 配置获取连接等待超时的时间
  92. maxWait: 60000
  93. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  94. timeBetweenEvictionRunsMillis: 60000
  95. # 配置一个连接在池中最小生存的时间,单位是毫秒
  96. minEvictableIdleTimeMillis: 300000
  97. validationQuery: SELECT 1 FROM DUAL
  98. testWhileIdle: true
  99. testOnBorrow: false
  100. testOnReturn: false
  101. # 打开PSCache,并且指定每个连接上PSCache的大小
  102. poolPreparedStatements: true
  103. maxPoolPreparedStatementPerConnectionSize: 20
  104. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  105. filters: stat,wall,slf4j
  106. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  107. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

测试

这里的测试就比较简单了,比如可以写一个 controller,只要改变 Db02DataSourceRouter 中的 currentKey 参数即可

  1. @Autowired
  2. @Qualifier("db02DataSource")
  3. private DataSource db02DataSourceRouter;
  4. @ApiOperation("数据源配置切换测试")
  5. @PostMapping("ds-switch")
  6. public Result dsSwitch(int index) {
  7. Db02DataSourceRouter router = (Db02DataSourceRouter) mlsDataSourceRouter;
  8. // 设置当前生效的数据源
  9. router.setCurrentKey(index == 1 ? "ds1" : "ds2");
  10. return ResultHelper.ok(router.getCurrentKey());
  11. }

遗留的问题

配置打印 myabtis 相关的日志信息

  1. mybatis:
  2. configuration:
  3. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

在获取链接的时候能看到如下 debug 的日志信息

  1. was not registered for synchronization because synchronization is not active
  2. Creating a new SqlSession

以上报错信息是在 org.mybatis.spring.SqlSessionUtils#registerSessionHolder 中 debug 信息,暂时不明白具体是什么原因导致的,但是不影响功能的使用。后续有时间,或则有谁知道的请给我留言。