springboot 動態數據源(Mybatis+Druid)

git代碼地址java

Spring多數據源實現的方式大概有2中,一種是新建多個MapperScan掃描不一樣包,另一種則是經過繼承AbstractRoutingDataSource實現動態路由。今天做者主要基於後者作的實現,且方式1的實現比較簡單這裏不作過多探討。mysql

實現方式

方式1的實現(核心代碼):git

@Configuration
@MapperScan(basePackages = "com.goofly.test1", sqlSessionTemplateRef  = "test1SqlSessionTemplate")
public class DataSource1Config1 {

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    @Primary
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }
    // .....略

}

@Configuration
@MapperScan(basePackages = "com.goofly.test2", sqlSessionTemplateRef  = "test1SqlSessionTemplate")
public class DataSourceConfig2 {

    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.test2")
    @Primary
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }
    // .....略

}

方式2的實現(核心代碼):github

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class);
    
    @Override
    protected Object determineCurrentLookupKey() {
        //從ThreadLocal中取值
        return DynamicDataSourceContextHolder.get();
    }
}
​ 第1種方式雖然實現比較加單,劣勢就是不一樣數據源的mapper文件不能在同一包名,就顯得不太靈活了。因此爲了更加靈活的做爲一個組件的存在,做者採用的第二種方式實現。

設計思路

  1. 當請求通過被註解修飾的類後,此時會進入到切面邏輯中。
  2. 切面邏輯會獲取註解中設置的key值,而後將該值存入到ThreadLocal
  3. 執行完切面邏輯後,會執行AbstractRoutingDataSource.determineCurrentLookupKey()方法,而後從ThreadLocal中獲取以前設置的key值,而後將該值返回。
  4. 因爲AbstractRoutingDataSourcetargetDataSources是一個map,保存了數據源key和數據源的對應關係,因此可以順利的找到該對應的數據源。

源碼解讀

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,以下:spring

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    
    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
    
        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.
     */
    protected abstract Object determineCurrentLookupKey();
    
    //........略

targetDataSources是一個map結構,保存了key與數據源的對應關係;sql

dataSourceLookup是一個DataSourceLookup類型,默認實現是JndiDataSourceLookup。點開該類源碼會發現,它實現了經過key獲取DataSource的邏輯。固然,這裏能夠經過setDataSourceLookup()來改變其屬性,由於關於此處有一個坑,後面會講到。app

public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup {

    public JndiDataSourceLookup() {
        setResourceRef(true);
    }

    @Override
    public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException {
        try {
            return lookup(dataSourceName, DataSource.class);
        }
        catch (NamingException ex) {
            throw new DataSourceLookupFailureException(
                    "Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex);
        }
    }

}

組件使用

多數據源

# db1
spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true
## db2
spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.slave.username = root
spring.datasource.slave.password = 123456
spring.datasource.slave.driverClassName = com.mysql.jdbc.Driver
spring.datasource.slave.validationQuery = true
spring.datasource.slave.testOnBorrow = true

#主數據源名稱
spring.maindb=master
#mapperper包路徑
mapper.basePackages =com.btps.xli.multidb.demo.mapper

單數據源

爲了讓使用者可以用最小的改動實現最好的效果,做者對單數據源的多種配置作了兼容。ide

示例配置1(配置數據源名稱):性能

spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.master.username = root
spring.datasource.master.password = 123456
spring.datasource.master.driverClassName = com.mysql.jdbc.Driver
spring.datasource.master.validationQuery = true
spring.datasource.master.testOnBorrow = true

# mapper包路徑
mapper.basePackages = com.goofly.xli.multidb.demo.mapper
# 主數據源名稱
spring.maindb=master

示例配置2(不配置數據源名稱):ui

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.validationQuery = true
spring.datasource.testOnBorrow = true

# mapper包路徑
mapper.basePackages = com.goofly.xli.multidb.demo.mapper

踩坑之路

多數據源的循環依賴

Description:

The dependencies of some of the beans in the application context form a cycle:

   happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService)
      ↓
   happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao)
      ↓
   masterDao defined in file [E:\GitRepository\framework-gray\test-db\target\classes\com\db\mapper\MasterDao.class]
      ↓
   sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
┌─────┐
|  dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑     ↓
|  firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]
↑     ↓
|  dataSourceInitializer

解決方案:

在Spring boot啓動的時候排除DataSourceAutoConfiguration便可。以下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DBMain {
    public static void main(String[] args) {
        SpringApplication.run(DBMain.class, args);
    }
}
​ 可是做者在建立多數據源的時候因爲並未建立多個 DataSource的Bean,而是隻建立了一個即須要作動態數據源的那個Bean。 其餘的 DataSource則直接建立實例而後存放在Map裏面,而後再設置到 DynamicRoutingDataSource#setTargetDataSources便可。

所以這種方式也不會出現循環依賴的問題!

動態刷新數據源

​ 筆者在設計之初是想構建一個動態刷新數據源的方案,因此利用了 SpringCloud@RefreshScope去標註數據源,而後利用 RefreshScope#refresh實現刷新。可是在實驗的時候發現由Druid建立的數據源會所以而關閉,由Spring的 DataSourceBuilder建立的數據源則不會發生任何變化。

​ 最後關於此也沒能找到解決方案。同時思考,若是隻能的能夠實現動態刷新的話,那麼數據源的原有鏈接會由於刷新而中斷嗎仍是會有其餘處理?

多數據源事務

​ 有這麼一種特殊狀況,一個事務中調用了兩個不一樣數據源,這個時候動態切換數據源會所以而失效。

翻閱了不少文章,大概找了2中解決方案,一種是Atomikos進行事務管理,可是貌似性能並非很理想。

另一種則是經過優先級控制,切面的的優先級必需要大於數據源的優先級,用註解@Order控制。

此處留一個坑!

相關文章
相關標籤/搜索