搞定SpringBoot多數據源(2):動態數據源

一句話概括:使用動態數據源對多個數據庫進行操作,靈活,簡潔。

1. 引言

對於多個數據庫的處理,上一篇文章《搞定SpringBoot多數據源(1):多套源策略》已有提及,有多套數據源、動態數據源、參數化變更數據源等方式,本文是第二篇:“動態數據源”。動態數據源可以解決多套數據源的處理不夠靈活、佔用資源多等問題。用戶可以根據實際的業務需要,統一操作邏輯,只要在需要切換數據源的進行處理即可。何爲動態,其實是批切換數據源的時機可以動態選擇,在需要的地方進行切換即可。

本文延續上一篇文章的示例,以主從場景爲示例,結合代碼,對動態數據源的實現進行講解,內容包括搭建動態數據源原理、動態數據源配置、動態數據源使用,AOP 註解方式切換數據源等。

本文所涉及到的示例代碼:https://github.com/mianshenglee/my-example/tree/master/multi-datasource,讀者可結合一起看。

2. 動態數據源流程說明

Spring Boot 的動態數據源,本質上是把多個數據源存儲在一個 Map 中,當需要使用某個數據源時,從 Map 中獲取此數據源進行處理。而在 Spring 中,已提供了抽象類 AbstractRoutingDataSource 來實現此功能。因此,我們在實現動態數據源的,只需要繼承它,實現自己的獲取數據源邏輯即可。動態數據源流程如下所示:

動態數據源

用戶訪問應用,在需要訪問不同的數據源時,根據自己的數據源路由邏輯,訪問不同的數據源,實現對應數據源的操作。本示例中的兩數據庫的分別有一個表 test_user,表結構一致,爲便於說明,兩個表中的數據是不一樣的。兩個表結構可在示例代碼中的 sql 目錄中獲取。

3. 實現動態數據源

3.1 說明及數據源配置

3.1.1 包結構說明

本示例中,主要有以下幾個包:

├─annotation ---- // 自定義註解
├─aop ----------- // 切面
├─config -------- // 數據源配置
├─constants ----- // 常用註解
├─context ------- // 自定義上下文
├─controller ---- // 訪問接口
├─entity -------- // 實體
├─mapper -------- // 數據庫dao操作
├─service ------- // 服務類
└─vo ------------ // 視圖返回數據

3.1.2 數據庫連接信息配置

Spring Boot 的默認配置文件是 application.properties ,由於有兩個數據庫配置,獨立配置數據庫是好的實踐,因此添加配置文件 jbdc.properties ,添加以下自定義的主從數據庫配置:

# master
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/mytest?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.master.username=root
spring.datasource.master.password=111111

# slave
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/my_test1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.slave.username=root
spring.datasource.slave.password=111111

3.1.3 數據源配置

根據連接信息,把數據源注入到 Spring 中,添加 DynamicDataSourceConfig 文件,配置如下:

@Configuration
@PropertySource("classpath:config/jdbc.properties")
@MapperScan(basePackages = "me.mason.demo.dynamicdatasource.mapper")
public class DynamicDataSourceConfig {
    @Bean(DataSourceConstants.DS_KEY_MASTER)
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(DataSourceConstants.DS_KEY_SLAVE)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
}

注意:

  • 此處使用 PropertySource 指定配置文件,ConfigurationProperties 指定數據源配置前綴
  • 使用 MapperScan 指定包,自動注入相應的 mapper 類。
  • 把數據源常量寫在 DataSourceConstants 類中
  • 從此配置可以看到,已經把 SqlSessionFactory 這個配置從代碼中擦除,直接使用 Spring Boot 自動配置的 SqlSessionFactory 即可,無需我們自己配置。

3.2 動態數據源設置

前面的配置已把多個數據源注入到 Spring 中,接着對動態數據源進行配置。

3.2.1 動態數據源配置

** (1) 添加jdbc依賴 **

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

** (2) 添加動態數據源類 **

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 此處暫時返回固定 master 數據源, 後面按動態策略修改
        return DataSourceConstants.DS_KEY_MASTER;
    }
}

注意:

  • 繼承抽象類 AbstractRoutingDataSource ,需要實現方法 determineCurrentLookupKey,即路由策略。
  • 動態路由策略下一步實現,當前策略直接返回 master 數據源

(3) 設置動態數據源爲主數據源

在前面的數據源配置文件 DynamicDataSourceConfig 中,添加以下代碼:

@Bean
@Primary
public DataSource dynamicDataSource() {
    Map<Object, Object> dataSourceMap = new HashMap<>(2);
    dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
    dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
    //設置動態數據源
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    dynamicDataSource.setTargetDataSources(dataSourceMap);
    dynamicDataSource.setDefaultTargetDataSource(masterDataSource());

    return dynamicDataSource;
}
  • 使用 Map 保存多個數據源,並設置到動態數據源對象中。
  • 設置默認的數據源是 master 數據源
  • 使用註解 Primary 優先從動態數據源中獲取

同時,需要在 DynamicDataSourceConfig 中,排除 DataSourceAutoConfiguration 的自動配置,否則 會出現The dependencies of some of the beans in the application context form a cycle的錯誤。

@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })

3.2.2 動態選擇數據源

(1) 數據源 key 的上下文

前面固定寫了一個數據源路由策略,總是返回 master,顯然不是我們想要的。我們想要的是在需要的地方,想切換就切換。因此,需要有一個動態獲取數據源 key 的地方(我們稱爲上下文),對於 web 應用,訪問以線程爲單位,使用 ThreadLocal 就比較合適,如下:

public class DynamicDataSourceContextHolder {
    /**
     * 動態數據源名稱上下文
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();
    /**
     * 設置/切換數據源
     */
    public static void setContextKey(String key){
        DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
    }
    /**
     * 獲取數據源名稱
     */
    public static String getContextKey(){
        String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
        return key == null?DataSourceConstants.DS_KEY_MASTER:key;
    }

    /**
     * 刪除當前數據源名稱
     */
    public static void removeContextKey(){
        DATASOURCE_CONTEXT_KEY_HOLDER.remove();
    }
  • 以 DATASOURCE_CONTEXT_KEY_HOLDER 存儲需要使用數據源 key

  • getContextKey 時,若 key 爲空,默認返回 master

(2) 設置動態數據 DynamicDataSource 路由策略

我們需要達到的路由策略是,當設置數據源 key 到上下文,則從上下文中得到此數據源 key ,從而知道使用此對應的數據源。因此,修改前面 DynamicDataSourcedetermineCurrentLookupKey 方法如下:

@Override
protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getContextKey();
}

3.2.3 動態數據源使用

有了上面的動態路由選擇,則不需要像之前的多套數據源那樣,mapper、entity、service等都寫一套相同邏輯的代碼,因爲是主從,一般來說數據庫結構是一致的,只需要一套entity、mapper、service即可,在需要在不同的數據源進行操作時,直接對上下文進行設置即可。如下:

@RestController
@RequestMapping("/user")
public class TestUserController {

    @Autowired
    private TestUserMapper testUserMapper;

    /**
     * 查詢全部
     */
    @GetMapping("/listall")
    public Object listAll() {
        int initSize = 2;
        Map<String, Object> result = new HashMap<>(initSize);
        //默認master查詢
        QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
        List<TestUser> resultData = testUserMapper.selectAll(queryWrapper.isNotNull("name"));
        result.put(DataSourceConstants.DS_KEY_MASTER, resultData);

        //切換數據源,在slave查詢
        DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DS_KEY_SLAVE);
        List<TestUser> resultDataSlave = testUserMapper.selectList(null);
        result.put(DataSourceConstants.DS_KEY_SLAVE, resultDataSlave);
        //恢復數據源
        DynamicDataSourceContextHolder.removeContextKey();
        //返回數據
        return ResponseResult.success(result);
    }

}
  • 默認是使用 master 數據源查詢
  • 使用上下文的 setContextKey 來切換數據源,使用完後使用 removeContextKey 進行恢復

3.3 使用 AOP 選擇數據源

經過上面的動態數據源配置,可以實現動態數據源切換,但我們會發現,在進行數據源切換時,都需要做 setContextKeyremoveContextKey 操作,如果需要切換的方法比多,就會發現很多重複的代碼,如何消除這些重複的代碼,就需要用到動態代理了,如果不瞭解動態代理,可以參考一下我的這篇文章《java開發必學知識:動態代理》。在 Spring 中,AOP 的實現也是基於動態代理的。此處,我們希望通過註解的方式指定函數需要的數據源,從而消除數據源切換時產品的模板代碼。

3.3.1 定義數據源註解

annotation包中,添加數據源註解 DS,此註解可以寫在類中,也可以寫在方法定義中,如下所示:

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
    /**
     * 數據源名稱
     */
    String value() default DataSourceConstants.DS_KEY_MASTER;
}

3.3.2 定義數據源切面

定義數據源切面,此切面可以針對使用了 DS 註解的方法或者類,進行數據源切換。

(1)添加aop依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(2) 定義切面

@Aspect
@Component
public class DynamicDataSourceAspect {
    @Pointcut("@annotation(me.mason.demo.dynamicdatasource.annotation.DS)")
    public void dataSourcePointCut(){

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String dsKey = getDSAnnotation(joinPoint).value();
        DynamicDataSourceContextHolder.setContextKey(dsKey);
        try{
            return joinPoint.proceed();
        }finally {
            DynamicDataSourceContextHolder.removeContextKey();
        }
    }

    /**
     * 根據類或方法獲取數據源註解
     */
    private DS getDSAnnotation(ProceedingJoinPoint joinPoint){
        Class<?> targetClass = joinPoint.getTarget().getClass();
        DS dsAnnotation = targetClass.getAnnotation(DS.class);
        // 先判斷類的註解,再判斷方法註解
        if(Objects.nonNull(dsAnnotation)){
            return dsAnnotation;
        }else{
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            return methodSignature.getMethod().getAnnotation(DS.class);
        }
    }
}
  • 註解 Pointcut 使用 annotation 指定註解
  • 註解 Around 使用環繞通知處理,使用上下文進行對使用註解 DS 的值進行數據源切換,處理完後,恢復數據源。

3.3.3 使用 AOP 進行數據源切換

在service層,我們定義一個 TestUserService ,裏面有兩個方法,分別是從 master 和 slave 中獲取數據,使用了註解DS,如下:

/**
 * 查詢master庫User
 */
@DS(DataSourceConstants.DS_KEY_MASTER)
public List<TestUser> getMasterUser(){
    QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
    return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
}

/**
 * 查詢slave庫User
 */
@DS(DataSourceConstants.DS_KEY_SLAVE)
public List<TestUser> getSlaveUser(){ return testUserMapper.selectList(null); }

這樣定義後,在 controller 層的處理就可以變成:

@GetMapping("/listall")
public Object listAll() {
    int initSize = 2;
    Map<String, Object> result = new HashMap<>(initSize);
    //默認master數據源查詢
    List<TestUser> masterUser = testUserService.getMasterUser();
    result.put(DataSourceConstants.DS_KEY_MASTER, masterUser);
    //從slave數據源查詢
    List<TestUser> slaveUser = testUserService.getSlaveUser();
    result.put(DataSourceConstants.DS_KEY_SLAVE, slaveUser);
    //返回數據
    return ResponseResult.success(result);
}

由此可見,已經把數據庫切換的模板代碼消除,只需要關注業務邏輯處理即可。這就是AOP的好處。

4. 再思考一下

經過上面的動態數據源及 AOP 選擇數據源的講解,我們可以看到動態數據源已經很靈活,想切換隻需在上下文中進行設置數據源即可,也可以直接在方法或類中使用註解來完成。現在我們是手動編碼實現的,其實,對於MyBatis Plus ,它也提供了一個動態數據源的插件,有興趣的小夥伴也可以根據它的官方文檔進行實驗使用。

對於動態數據源,還有哪些地方需要考慮或者說值得改進的地方呢?

  • 事務如何處理?其實在開發中應該儘量避免跨庫事務,但如果避免不了,則需要使用分佈式事務。
  • 對於當前的動態數據源,相對來說還是固定的數據源(如一主一從,一主多從等),即在編碼時已經確定的數據庫數量,只是在具體使用哪一個時進行動態處理。如果數據源本身並不確定,或者說需要根據用戶輸入來連接數據庫,這時,如何處理呢?這種情況出現得比較多的是在對多個數據庫進行管理時的處理。這種情況,我將在下一篇文章中進行講解,我把它叫做"參數化變更數據源"。

5. 總結

本文對動態數據源的實現進行了講解,主要是動態數據源的配置、實現、使用,另外還使用 AOP 消除切換數據源時的模板代碼,使我們開發專注於業務代碼,最後對動態數據源的進行了一下擴展思考。希望小夥伴們可以掌握動態數據源的處理。

本文配套的示例,示例代碼,有興趣的可以運行示例來感受一下。

參考資料

往期文章

我的公衆號(搜索Mason技術記錄),獲取更多技術記錄:

mason

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