一句話概括:使用動態數據源對多個數據庫進行操作,靈活,簡潔。
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 ,從而知道使用此對應的數據源。因此,修改前面 DynamicDataSource
的 determineCurrentLookupKey
方法如下:
@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 選擇數據源
經過上面的動態數據源配置,可以實現動態數據源切換,但我們會發現,在進行數據源切換時,都需要做 setContextKey
和 removeContextKey
操作,如果需要切換的方法比多,就會發現很多重復的代碼,如何消除這些重復的代碼,就需要用到動態代理了,如果不了解動態代理,可以參考一下我的這篇文章《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 消除切換數據源時的模板代碼,使我們開發專注於業務代碼,最后對動態數據源的進行了一下擴展思考。希望小伙伴們可以掌握動態數據源的處理。
本文配套的示例,示例代碼,有興趣的可以運行示例來感受一下。
參考資料
- Spring主從數據庫的配置和動態數據源切換原理:
https://www.liaoxuefeng.com/article/1182502273240832
- 多數據源與動態數據源的權衡:
https://juejin.im/post/5b790a866fb9a019ea01f38c
- 談談Spring Boot 數據源加載及其多數據源簡單實現:
https://juejin.im/post/5cb0023d5188250df17d4ffc
- Spring Boot 和 MyBatis 實現多數據源、動態數據源切換:
https://juejin.im/post/5a927d23f265da4e7e10d740
往期文章
- 搞定SpringBoot多數據源(1):多套源策略
- java開發必學知識:動態代理
- 2019 讀過的好書推薦
- springboot+apache前后端分離部署https
- springboot+logback 日志輸出企業實踐(下)
- springboot+logback 日志輸出企業實踐(上)
我的公眾號(搜索Mason技術記錄
),獲取更多技術記錄: