• Spring boot多数据源实现动态切换


    概述

    日常的业务开发项目中只会配置一套数据源,如果需要获取其他系统的数据往往是通过调用接口, 或者是通过第三方工具比如kettle将数据同步到自己的数据库中进行访问。

    但是也会有需要在项目中引用多数据源的场景。比如如下场景:

    • 自研数据迁移系统,至少需要新、老两套数据源,从老库读取数据写入新库
    • 自研读写分离中间件,系统流量增加,单库响应效率降低,引入读写分离方案,写入数据是一个数据源,读取数据是另一个数据源

    环境说明

    • spring boot
    • mysql
    • mybatis-plus
    • spring-aop

    项目目录结构

    • controller: 存放接口类
    • service: 存放服务类
    • mapper: 存放操作数据库的mapper接口
    • entity: 存放数据库表实体类
    • vo: 存放返回给前端的视图类

    • context: 存放持有当前线程数据源key类
    • constants: 存放定义数据源key常量类
    • config: 存放数据源配置类
    • annotation: 存放动态数据源注解
    • aop: 存放动态数据源注解切面类
    • resources.config: 项目配置文件
    • resources.mapper: 数据库xml文件

    关键类说明

    忽略掉controller/service/entity/mapper/xml介绍。

    • jdbc.properties: 数据源配置文件。虽然可以配置到Spring boot的默认配置文件application.properties/application.yml文件当中,但是如果数据源比较多的话,根据实际使用,最佳的配置方式还是独立配置比较好。
    • DynamicDataSourceConfig: 数据源配置类
    • DynamicDataSource: 动态数据源配置类
    • DataSourceRouting: 动态数据源注解
    • DynamicDataSourceAspect: 动态数据源设置切面
    • DynamicDataSourceContextHolder: 当前线程持有的数据源key
    • DataSourceConstants: 数据源key常量类

    开发流程

    动态数据源流程

    Spring Boot 的动态数据源,本质上是把多个数据源存储在一个 Map 中,当需要使用某个数据源时,从 Map 中获取此数据源进行处理。

    在 Spring 中已提供了抽象类 AbstractRoutingDataSource 来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法即可,该方法只需要返回数据源key即可,也就是存放数据源的Map的key。

    因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。AbstractRoutingDataSource顶级继承了DataSource,所以它也是可以做为数据源对象,因此项目中使用它作为主数据源。

    AbstractRoutingDataSource原理

    AbstractRoutingDataSource中有一个重要的属性:

    • targetDataSources: 目标数据源,即项目启动的时候设置的需要通过AbstractRoutingDataSource管理的数据源。
    • defaultTargetDataSource: 默认数据源,项目启动的时候设置的默认数据源,如果没有指定数据源,默认返回改数据源。
    • resolvedDataSources: 也是存放的数据源,是对targetDataSources进行处理后进行存储的。可以看一下源码。

    • resolvedDefaultDataSource: 对默认数据源进行了二次处理,源码如上图最后的两行代码。

    AbstractRoutingDataSource中所有的方法和属性:

    比较重要的是determineTargetDataSource方法。

    1. protected DataSource determineTargetDataSource() {
    2. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    3. Object lookupKey = determineCurrentLookupKey();
    4. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    5. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
    6. dataSource = this.resolvedDefaultDataSource;
    7. }
    8. if (dataSource == null) {
    9. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    10. }
    11. return dataSource;
    12. }
    13. /**
    14. * Determine the current lookup key. This will typically be
    15. * implemented to check a thread-bound transaction context.
    16. *

      Allows for arbitrary keys. The returned key needs

    17. * to match the stored lookup key type, as resolved by the
    18. * {@link #resolveSpecifiedLookupKey} method.
    19. */
    20. @Nullable
    21. protected abstract Object determineCurrentLookupKey();

    这个方法主要就是返回一个DataSource对象,主要逻辑就是先通过方法determineCurrentLookupKey获取一个Object对象的lookupKey,然后通过这个lookupKey到resolvedDataSources中获取数据源(resolvedDataSources就是一个Map,上面已经提到过了);如果没有找到数据源,就返回默认的数据源。determineCurrentLookupKey就是程序员配置动态数据源需要自己实现的方法。


    问题

    配置多数据源后启动项目报错:Property 'sqlSessionFactory' or 'sqlSession Template' are required。

    翻译过来就是:需要属性“sqlSessionFactory”或“sqlSessionTemplate”。也就是说 注入数据源的时候需要这两个数据,但是这两个属性在启动容器中没有找到。

    当引入mybatis-plus依赖mybatis-plus-boot-starter后,会添加一个自动配置类 MybatisPlusAutoConfiguration 。 其中有两个方法sqlSessionFactory()sqlSessionTemplate()。这两个方法就是 给容器中注入“sqlSessionFactory”或“sqlSessionTemplate”两个属性。

    1. @Configuration(
    2. proxyBeanMethods = false
    3. )
    4. @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
    5. @ConditionalOnSingleCandidate(DataSource.class)
    6. @EnableConfigurationProperties({MybatisPlusProperties.class})
    7. @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
    8. public class MybatisPlusAutoConfiguration implements InitializingBean {
    9. @Bean
    10. @ConditionalOnMissingBean
    11. public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    12. MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
    13. factory.setDataSource(dataSource);
    14. factory.setVfs(SpringBootVFS.class);
    15. if (StringUtils.hasText(this.properties.getConfigLocation())) {
    16. factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    17. }
    18. this.applyConfiguration(factory);
    19. if (this.properties.getConfigurationProperties() != null) {
    20. factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    21. }
    22. if (!ObjectUtils.isEmpty(this.interceptors)) {
    23. factory.setPlugins(this.interceptors);
    24. }
    25. if (this.databaseIdProvider != null) {
    26. factory.setDatabaseIdProvider(this.databaseIdProvider);
    27. }
    28. if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
    29. factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    30. }
    31. if (this.properties.getTypeAliasesSuperType() != null) {
    32. factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
    33. }
    34. if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
    35. factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    36. }
    37. if (!ObjectUtils.isEmpty(this.typeHandlers)) {
    38. factory.setTypeHandlers(this.typeHandlers);
    39. }
    40. if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
    41. factory.setMapperLocations(this.properties.resolveMapperLocations());
    42. }
    43. Objects.requireNonNull(factory);
    44. this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
    45. Class defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    46. if (!ObjectUtils.isEmpty(this.languageDrivers)) {
    47. factory.setScriptingLanguageDrivers(this.languageDrivers);
    48. }
    49. Optional var10000 = Optional.ofNullable(defaultLanguageDriver);
    50. Objects.requireNonNull(factory);
    51. var10000.ifPresent(factory::setDefaultScriptingLanguageDriver);
    52. this.applySqlSessionFactoryBeanCustomizers(factory);
    53. GlobalConfig globalConfig = this.properties.getGlobalConfig();
    54. Objects.requireNonNull(globalConfig);
    55. this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
    56. this.getBeansThen(IKeyGenerator.class, (i) -> {
    57. globalConfig.getDbConfig().setKeyGenerators(i);
    58. });
    59. Objects.requireNonNull(globalConfig);
    60. this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
    61. Objects.requireNonNull(globalConfig);
    62. this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
    63. factory.setGlobalConfig(globalConfig);
    64. return factory.getObject();
    65. }
    66. @Bean
    67. @ConditionalOnMissingBean
    68. public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    69. ExecutorType executorType = this.properties.getExecutorType();
    70. return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    71. }
    72. }

    这里主要关注配置类上面的注解,详细如下:

    1. @Configuration(
    2. proxyBeanMethods = false
    3. )
    4. // 当类路径下有SqlSessionFactory.class、SqlSessionFactoryBean.class时才生效
    5. @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
    6. // 容器中只能有一个符合条件的DataSource
    7. // 因为容器中有3个数据源,且没有指定主数据源,这个条件不通过,就不会初始化这个配置类了
    8. @ConditionalOnSingleCandidate(DataSource.class)
    9. @EnableConfigurationProperties({MybatisPlusProperties.class})
    10. @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
    11. public class MybatisPlusAutoConfiguration implements InitializingBean {
    12. }

    因为MybatisPlusAutoConfiguration不满足@ConditionalOnSingleCandidate(DataSource.class) 条件,所以自动配置不会生效,就不会执行sqlSessionFactory()sqlSessionTemplate()两个方法。 容器中就不会有sqlSessionFactorysqlSessionTemplate 两个bean对象,因此会报错。

    所以在数据源配置类中的动态数据源配置上添加@Primary注解即可。

    1. @Configuration
    2. @PropertySource("classpath:config/jdbc.properties")
    3. @MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper"})
    4. public class DynamicDataSourceConfig {
    5. @Bean
    6. @Primary
    7. public DataSource dynamicDataSource() {
    8. Map<Object, Object> targetDataSource = new HashMap<>();
    9. targetDataSource.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
    10. targetDataSource.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
    11. DynamicDataSource dataSource = new DynamicDataSource();
    12. dataSource.setTargetDataSources(targetDataSource);
    13. dataSource.setDefaultTargetDataSource(masterDataSource());
    14. return dataSource;
    15. }
    16. }
  • 相关阅读:
    【Windows】Win 10下的 PS/2 接口的键鼠连接问题
    数据库容灾GoldenGate
    【算法挨揍日记】day14——724. 寻找数组的中心下标、238. 除自身以外数组的乘积
    Enterprise Architect15(EA) 工具栏,隐藏后显示快捷方式
    C/C++开发,opencv-ml库学习,随机森林(RTrees)应用
    C#入门:简单数据类型和强制类型转换
    面试题:Flink反压机制及与Spark Streaming的区别
    LeetCode-698. 划分为k个相等的子集【数组,回溯】
    李沐67_自注意力——自学笔记
    java[线程]volatile为什么不能保证原子性
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126115608