目錄html
一句話歸納:使用動態數據源對多個數據庫進行操做,靈活,簡潔。java
對於多個數據庫的處理,上一篇文章《搞定SpringBoot多數據源(1):多套源策略》已有說起,有多套數據源、動態數據源、參數化變動數據源等方式,本文是第二篇:「動態數據源」。動態數據源能夠解決多套數據源的處理不夠靈活、佔用資源多等問題。用戶能夠根據實際的業務須要,統一操做邏輯,只要在須要切換數據源的進行處理便可。何爲動態,實際上是批切換數據源的時機能夠動態選擇,在須要的地方進行切換便可。mysql
本文延續上一篇文章的示例,以主從場景爲示例,結合代碼,對動態數據源的實現進行講解,內容包括搭建動態數據源原理、動態數據源配置、動態數據源使用,AOP 註解方式切換數據源等。git
本文所涉及到的示例代碼:https://github.com/mianshenglee/my-example/tree/master/multi-datasource
,讀者可結合一塊兒看。github
Spring Boot 的動態數據源,本質上是把多個數據源存儲在一個 Map 中,當須要使用某個數據源時,從 Map 中獲取此數據源進行處理。而在 Spring 中,已提供了抽象類 AbstractRoutingDataSource
來實現此功能。所以,咱們在實現動態數據源的,只須要繼承它,實現本身的獲取數據源邏輯便可。動態數據源流程以下所示:web
用戶訪問應用,在須要訪問不一樣的數據源時,根據本身的數據源路由邏輯,訪問不一樣的數據源,實現對應數據源的操做。本示例中的兩數據庫的分別有一個表 test_user
,表結構一致,爲便於說明,兩個表中的數據是不同的。兩個表結構可在示例代碼中的 sql
目錄中獲取。spring
本示例中,主要有如下幾個包:sql
├─annotation ---- // 自定義註解 ├─aop ----------- // 切面 ├─config -------- // 數據源配置 ├─constants ----- // 經常使用註解 ├─context ------- // 自定義上下文 ├─controller ---- // 訪問接口 ├─entity -------- // 實體 ├─mapper -------- // 數據庫dao操做 ├─service ------- // 服務類 └─vo ------------ // 視圖返回數據
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
根據鏈接信息,把數據源注入到 Spring 中,添加 DynamicDataSourceConfig
文件,配置以下:apache
@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 便可,無需咱們本身配置。
前面的配置已把多個數據源注入到 Spring 中,接着對動態數據源進行配置。
** (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 })
(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(); }
有了上面的動態路由選擇,則不須要像以前的多套數據源那樣,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 進行恢復
通過上面的動態數據源配置,能夠實現動態數據源切換,但咱們會發現,在進行數據源切換時,都須要作 setContextKey
和 removeContextKey
操做,若是須要切換的方法比多,就會發現不少重複的代碼,如何消除這些重複的代碼,就須要用到動態代理了,若是不瞭解動態代理,能夠參考一下個人這篇文章《java開發必學知識:動態代理》。在 Spring 中,AOP 的實現也是基於動態代理的。此處,咱們但願經過註解的方式指定函數須要的數據源,從而消除數據源切換時產品的模板代碼。
在annotation
包中,添加數據源註解 DS
,此註解能夠寫在類中,也能夠寫在方法定義中,以下所示:
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DS { /** * 數據源名稱 */ String value() default DataSourceConstants.DS_KEY_MASTER; }
定義數據源切面,此切面能夠針對使用了 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
的值進行數據源切換,處理完後,恢復數據源。
在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的好處。
通過上面的動態數據源及 AOP 選擇數據源的講解,咱們能夠看到動態數據源已經很靈活,想切換隻需在上下文中進行設置數據源便可,也能夠直接在方法或類中使用註解來完成。如今咱們是手動編碼實現的,其實,對於MyBatis Plus ,它也提供了一個動態數據源的插件,有興趣的小夥伴也能夠根據它的官方文檔進行實驗使用。
對於動態數據源,還有哪些地方須要考慮或者說值得改進的地方呢?
本文對動態數據源的實現進行了講解,主要是動態數據源的配置、實現、使用,另外還使用 AOP 消除切換數據源時的模板代碼,使咱們開發專一於業務代碼,最後對動態數據源的進行了一下擴展思考。但願小夥伴們能夠掌握動態數據源的處理。
本文配套的示例,示例代碼,有興趣的能夠運行示例來感覺一下。
https://www.liaoxuefeng.com/article/1182502273240832
https://juejin.im/post/5b790a866fb9a019ea01f38c
https://juejin.im/post/5cb0023d5188250df17d4ffc
https://juejin.im/post/5a927d23f265da4e7e10d740
個人公衆號(搜索Mason技術記錄
),獲取更多技術記錄: