Spring Boot動態數據源切換

      在實際開發過程中,可能有這樣的情況,一個項目涉及到多個數據庫.有的數據需要插入這個數據庫,而有的數據需要插入到另一個數據庫裏面.所以接下來,就來教大家怎麼用最簡單的方式來實現這一需求.

      我們將會用Spring裏面的AOP設計來實現這一邏輯.通過把註解添加在方法上(DAO方法上)來實現數據源的動態切換.簡單來說就是當前方法操作的是哪個數據源,我們就需要在方法上添加註解標識使用哪個數據源.

      我們注意到在Spring2.0.1中引入了AbstractRoutingDataSource,該類充當了DataSource的路由中介,能有在運行時,根據某種key值來動態切換到真正的DataSource上.AbstractRoutingDataSource就是我們實現數據源動態切換的關鍵部分.

      接下來,爲了加深大家的理解,我們通過一個簡單的例子一步一步來實現這一過程.假設現在,我們有這樣一個需求.項目裏面需要到三個數據源(三個數據庫).

一 數據源枚舉

      我們用一個枚舉來表示這三個數據源.每個枚舉表示一個數據源,三個數據源,三個枚舉.如下:

public enum EDataSourceType {

    /**
     * 【基礎】數據庫數據源名稱
     */
    BASIC,

    /**
     * 【歷史】數據庫數據源名稱
     */
    HISTORY,

    /**
     * 【統計】數據庫數據源名稱
     */
    STATIS
}

二 數據源配置

      三個數據源肯定都有各自的url地址,所以配置文件(application.yml)配置如下:

spring:
  application:
    name: table-shard
  datasource:
    name: mysql_test
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 多數據源
      basic:  #數據源 -- 基礎數據庫
        url: jdbc:mysql://127.0.0.1:3306/basicdb?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      history:  #數據源 -- 歷史數據庫
        url: jdbc:mysql://127.0.0.1:3306/historydb?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      statis:  #數據源 -- 統計數據庫
        url: jdbc:mysql://127.0.0.1:3306/statisdb?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      filters: stat
      driver-class-name: com.mysql.cj.jdbc.Driver
      initial-size: 1
      min-idle: 1
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20

注意三個數據源的配置

三 數據源(DataSource)路由的實現

      AbstractRoutingDataSource是DataSource的路由中介的處理器.也是我們實現數據源動切換最關鍵的部分.我們自定義DynamicRoutingDataSource類繼承AbstractRoutingDataSource加入數據源切換的邏輯.

      在運行過程中Spring會調用AbstractRoutingDataSource.determineCurrentLookupKey()方法動態獲取到需要的數據源.DynamicRoutingDataSource構造函數需要兩個參數一個是默認的數據源,另一個是多數據源map.key是數據源標識也就是我們上面的數據源枚舉EDataSourceType,value是DataSource(每個數據源對應的DataSource我們會在配置文件裏面添加進來).這樣在運行(AOP)的時候我們只需要調用setDataSource()方法指定到我們需要的數據源上就可以了.

/**
 * @name: DynamicRoutingDataSource
 * @author: tuacy.
 * @date: 2019/6/24.
 * @version: 1.0
 * @Description: 動態數據源設置,每次訪問之前設置,訪問完成之後在清空
 * (AbstractRoutingDataSource相當於數據源路由中介,能有在運行時, 根據某種key值來動態切換到真正的DataSource上)
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<EDataSourceType> contextHolder = new ThreadLocal<>();

    /**
     * 構造函數
     *
     * @param defaultTargetDataSource 默認的數據源
     * @param targetDataSources       多數據源每個key對應一個數據源
     */
    public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        // 設置默認數據源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        // 設置多數據源. key value的形式
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 多數據源對應的key, 會通過這個key找到我們需要的數據源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    /**
     * 設置使用哪個數據源
     *
     * @param dataSource 數據源對應的名字
     */
    public static void setDataSource(EDataSourceType dataSource) {
        contextHolder.set(dataSource);
    }

    /**
     * 獲取數據源對應的名字
     * @return 數據源對應的名字
     */
    public static EDataSourceType getDataSource() {
        return contextHolder.get();
    }

    /**
     * 清空掉
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }

}

四 配置

4.1 DynamicDataSourceConfig配置

      把我們自定義的DynamicRoutingDataSource類注入到IOC容器裏面去.特別注意DynamicRoutingDataSource類的構造函需要兩個參數一個是默認的數據源,一個是多數據源map.注意map key是數據源標識(我們定義的數據源枚舉EDataSourceType),value是每個數據源對應的DataSource(url,password等配置信息).

public class DynamicDataSourceConfig {

    /**
     * 基礎數據庫   application.yml spring.datasource.druid.basic配置信息
     *
     * @return DataSource
     */
    @Bean(name = "basicDataSource")
    @ConfigurationProperties("spring.datasource.druid.basic")
    public DataSource basicDataSource() {
        return new DruidDataSource();
    }

    /**
     * 歷史數據庫  application.yml spring.datasource.druid.history配置信息
     *
     * @return DataSource
     */
    @Bean(name = "historyDataSource")
    @ConfigurationProperties("spring.datasource.druid.history")
    public DataSource historyDataSource() {
        return new DruidDataSource();
    }

    /**
     * 統計數據庫  application.yml  spring.datasource.druid.statis配置信息
     *
     * @return DataSource
     */
    @Bean(name = "statisDataSource")
    @ConfigurationProperties("spring.datasource.druid.statis")
    public DataSource statisDataSource() {
        return new DruidDataSource();
    }

    /**
     * 我們我們自定義的數據源DynamicRoutingDataSource添加到Spring容器裏面去
     *
     * @param basicDataSource   基礎數據庫
     * @param historyDataSource 歷史數據庫
     * @param statisDataSource  統計數據庫
     */
    @Bean
    @Primary
    public DynamicRoutingDataSource dataSource(DataSource basicDataSource, DataSource historyDataSource, DataSource statisDataSource) {
        Map<Object, Object> targetDataSources = Maps.newHashMapWithExpectedSize(3);
        // 每個key對應一個數據源
        targetDataSources.put(EDataSourceType.BASIC, basicDataSource);
        targetDataSources.put(EDataSourceType.HISTORY, historyDataSource);
        targetDataSources.put(EDataSourceType.STATIS, statisDataSource);
        return new DynamicRoutingDataSource(basicDataSource, targetDataSources);
    }

}

4.2 啓動類上的配置

      在啓動類上添加.過濾掉DataSourceAutoConfiguration配置,進入我們的配置DynamicDataSourceConfig.就是把默認的數據源配置替換成我們自定義的數據源配置.

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class
})
@Import({DynamicDataSourceConfig.class})

五 AOP

      AOP的實現部分就是在運行的時候去調用DynamicRoutingDataSource.setDataSource().我們把AOP註解添加在方法上,在進入方法之前我們先調用DynamicRoutingDataSource.setDataSource()來設置我們需要的數據源.實現邏輯如下.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceAnnotation {

    /**
     * 數據源類型
     * @return 數據源類型
     */
    EDataSourceType sourceType();
}
@Aspect
@Component
@Order(value = 1)
public class DataSourceAspect {

    /**
     * 所有添加了DataSourceAnnotation的方法都進入切面
     */
    @Pointcut("@annotation(com.tuacy.tableshard.tableextend.multisource.DataSourceAnnotation)")
    public void dataSourcePointCut() {
    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        // 在執行方法之前設置使用哪個數據源
        DataSourceAnnotation ds = method.getAnnotation(DataSourceAnnotation.class);
        if (ds == null) {
            DynamicRoutingDataSource.setDataSource(EDataSourceType.BASIC);
        } else {
            DynamicRoutingDataSource.setDataSource(ds.sourceType());
        }

        try {
            return point.proceed();
        } finally {
            DynamicRoutingDataSource.clearDataSource();
        }
    }
}

六 使用

      使用很簡單,就是在DAO對應的方法上添加@DataSourceAnnotation註解來指定數據源.這樣這個DAO方法裏面使用到的數據源就是我們指定的數據源.

    /**
     * DataSourceAnnotation 用於指定數據源,放到統計數據庫裏面
     */
    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public int insertItem(AccHour item) {
        return accHourMapper.insertItem(item);
    }

      上述動態數據源的切換整個邏輯我們就已經全部實現了,很簡單的.主要就是AbstractRoutingDataSource以及AOP的使用.更加詳細的代碼可以看下 https://github.com/tuacy/java-study/tree/master/tableshard

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章