天天看點

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多資料源實作動态切換

動态資料源流程

Spring Boot 的動态資料源,本質上是把多個資料源存儲在一個 Map 中,當需要使用某個資料源時,從 Map 中擷取此資料源進行處理。

Spring boot多資料源實作動态切換

在 Spring 中已提供了抽象類 AbstractRoutingDataSource 來實作此功能,繼承AbstractRoutingDataSource類并覆寫其​

​determineCurrentLookupKey()​

​方法即可,該方法隻需要傳回資料源key即可,也就是存放資料源的Map的key。

是以,我們在實作動态資料源的,隻需要繼承它,實作自己的擷取資料源邏輯即可。AbstractRoutingDataSource頂級繼承了DataSource,是以它也是可以做為資料源對象,是以項目中使用它作為主資料源。

Spring boot多資料源實作動态切換

AbstractRoutingDataSource原理

AbstractRoutingDataSource中有一個重要的屬性:

Spring boot多資料源實作動态切換
  • targetDataSources: 目标資料源,即項目啟動的時候設定的需要通過AbstractRoutingDataSource管理的資料源。
  • defaultTargetDataSource: 預設資料源,項目啟動的時候設定的預設資料源,如果沒有指定資料源,預設傳回改資料源。
  • resolvedDataSources: 也是存放的資料源,是對targetDataSources進行處理後進行存儲的。可以看一下源碼。
Spring boot多資料源實作動态切換
  • resolvedDefaultDataSource: 對預設資料源進行了二次處理,源碼如上圖最後的兩行代碼。

AbstractRoutingDataSource中所有的方法和屬性:

Spring boot多資料源實作動态切換

比較重要的是​

​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;
}

/**
* 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.
*/
@Nullable
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”兩個屬性。

@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        this.applyConfiguration(factory);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }

        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }

        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }

        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }

        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }

        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        Objects.requireNonNull(factory);
        this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }

        Optional var10000 = Optional.ofNullable(defaultLanguageDriver);
        Objects.requireNonNull(factory);
        var10000.ifPresent(factory::setDefaultScriptingLanguageDriver);
        this.applySqlSessionFactoryBeanCustomizers(factory);
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        this.getBeansThen(IKeyGenerator.class, (i) -> {
            globalConfig.getDbConfig().setKeyGenerators(i);
        });
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){
        ExecutorType executorType = this.properties.getExecutorType();
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }
}    
      
@Configuration(
        proxyBeanMethods = false
)
// 當類路徑下有SqlSessionFactory.class、SqlSessionFactoryBean.class時才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 容器中隻能有一個符合條件的DataSource
// 因為容器中有3個資料源,且沒有指定主資料源,這個條件不通過,就不會初始化這個配置類了
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {

}
      
@Configuration
@PropertySource("classpath:config/jdbc.properties")
@MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper"})
public class DynamicDataSourceConfig {
    @Bean
    @Primary
    public DataSource dynamicDataSource(){
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
        targetDataSource.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(masterDataSource());

        return dataSource;
    }
}