開心一刻
2023年元旦,我媽又開始了對我的唸叨
媽:你到底想多少歲結婚
我:60
媽:60,你想找個多大的
我:找個55的啊,她55我60,結婚都有退休金,不用上班不用生孩子,不用買車買房,成天就是玩兒
我:而且一結婚就是白頭偕老,多好
我媽直接一大嘴巴子呼我臉上
需求背景
最近接到一個需求,需要從兩個資料來源獲取資料,然後進行彙總展示
一個資料來源是 MySQL ,另一個資料來源是 SQL Server
樓主是一點都不慌的,因為我寫過好幾篇關於資料來源的文章
原理解密 → Spring AOP 實現動態資料來源(讀寫分離),底層原理是什麼
我會慌?
但還是有點小拒絕,為什麼了?
自己實現的話,要寫的東西還是很多,要寫 AOP ,還要實現 AbstractRoutingDataSource ,還要用到 ThreadLocal ,...
如果考慮更遠一些,事務、資料來源之間的巢狀等等,要如何保證正確?
但好在這次需求只是查詢,然後彙總,問題就簡單很多了,但還是覺得有點小繁瑣
當然,如上只是樓主的臆想
有小夥伴可能會問道:能不能合到一個資料來源?
樓主只能說:別問了,再問就不禮貌了
既然改變不了,那就盤它
難道就沒有現成的多資料來源工具?
因為用到了 Mybatis-Plus ,樓主試著 Google 了一下
直接一發入魂,眼前一黑,不對,是眼前一亮!
感覺就是它了!
MyBatis-Plus 多資料來源
關於苞米豆(baomidou),我們最熟悉的肯定是 MyBatis-Plus
但旗下還有很多其他優秀的元件
多資料來源就是其中一個,今天我們就來會會它
資料來源準備
用 docker 準備一個 MySQL 和 SQL Server ,圖省事,兩個資料庫伺服器放到同個 docker 下了
有小夥伴會覺得放一起不合適,有單點問題!
樓主只是為了演示,糾結那麼細,當心敲你狗頭
MySQL 版本: 8.0.27
建庫: datasource_mysql ,建表: tbl_user ,並插入初始化資料
CREATE DATABASE datasource_mysql; USE datasource_mysql; CREATE TABLE tbl_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_name VARCHAR(50), PRIMARY KEY(id) ); INSERT INTO tbl_user(user_name) VALUES('張三'),('李四');
SQL Server 版本: Microsoft SQL Server 2017 ... ,是真長,跟樓主一樣長!
建庫: datasource_mssql ,建表: tbl_order ,並插入初始化資料
CREATE DATABASE datasource_mssql; USE datasource_mssql; CREATE TABLE tbl_order( id BIGINT PRIMARY KEY IDENTITY(1,1), order_no NVARCHAR(50), created_at DATETIME NOT NULL DEFAULT(GETDATE()), updated_at DATETIME NOT NULL DEFAULT(GETDATE()) ); INSERT INTO tbl_order(order_no) VALUES('123456'),('654321');
dynamic-datasource 使用
基於 spring-boot 2.2.10.RELEASE 、 mybatis-plus 3.1.1 搭建
dynamic-datasource-spring-boot-starter 也是 3.1.1
依賴很簡單, pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lee</groupId> <artifactId>mybatis-plus-dynamic-datasource</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.10.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <mybatis-plus-boot-starter.version>3.1.1</mybatis-plus-boot-starter.version> <mssql-jdbc.version>6.2.1.jre8</mssql-jdbc.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus-boot-starter.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>${mybatis-plus-boot-starter.version}</version> </dependency> <!-- MySQL驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- SQL Server 驅動--> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>${mssql-jdbc.version}</version> </dependency> </dependencies> </project>
配置也很簡單, application.yml
server: port: 8081 spring: application: name: dynamic-datasource datasource: dynamic: datasource: mssql_db: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://10.5.108.225:1433;DatabaseName=datasource_mssql;IntegratedSecurity=false;ApplicationIntent=ReadOnly;MultiSubnetFailover=True username: sa password: Root#123456 mysql_db: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.5.108.225:3306/datasource_mysql?useSSL=false&useUnicode=true&characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai username: root password: 123456 primary: mssql_db strict: false mybatis-plus: mapper-locations: classpath:mappers/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
然後在對應的類或者方法上加上註解 DS("資料來源名稱") 即可,例如
我們來看下效果
是不是很神奇?
完整程式碼:mybatis-plus-dynamic-datasource
原理探究
@DS 用於指定資料來源,可以註解在方法上或類上,同時存在則採用就近原則 方法上註解 優先於 類上註解
這可不是我瞎說,官方文件就是這麼寫的
難道一個 @DS 就有如此強大的功能?你們不信,我也不信,它背後肯定有人!
那麼我們就來揪一揪背後的它
怎麼揪了,這又是個難題,我們先打個斷點,看一下呼叫棧
點一下,瞬間高潮了,不是,是瞬間清醒了
紅線框住的,分 2 點:1: determineDatasource ,2: DynamicDataSourceContextHolder.push
我們先看 determineDatasource
1、獲取 Method 物件
2、該方法上是否有 DS 註解,有則取方法的 DS 註解,沒有則取方法對應的類上的 DS 註解;這個看明白了沒?
3、獲取註解的值,也就是 @DS("mysql_db") 中的 mysql_db
4、如果資料來源名不為空並且資料原名以動態字首(#)開頭,則你們自己去跟 dsProcessor.determineDatasource
否則則直接返回資料來源名
針對案例的話,這裡肯定是返回類上的資料來源名(方法上沒有指定資料來源,也沒有以動態字首開頭)
我們再來看看 DynamicDataSourceContextHolder.push
很簡單,但 LOOKUP_KEY_HOLDER 很有意思
是一個棧,而非樓主在spring整合mybatis實現mysql讀寫分離 採用的
至於為什麼,人家註釋已經寫的很清楚了,試問樓主的實現能滿足一級一級資料來源切換的呼叫場景嗎?
但不管怎麼說, LOOKUP_KEY_HOLDER 的型別還是 ThreadLocal
接下來該分析什麼?
我們回顧下:原理解密 → Spring AOP 實現動態資料來源(讀寫分離),底層原理是什麼
直接跳到總結
框住的 3 條,上面的 2 條在上面已經分析過了把,是不是?你回答是就完事了
注意,樓主的 DynamicDataSource 是自實現的類,繼承了 spring-jdbc 的 AbstractRoutingDataSource
那我們就找 AbstractRoutingDataSource 的實現類唄
發現它就一個實現類,並且是在 spring-jdbc 下,而不是在 com.baomidou 下
莫非苞米豆有自己的 AbstractRoutingDataSource ? 我們來看看 AbstractDataSource 的實現類有哪些
看到了沒,那麼我們接下來就分析它
內容很簡單,最重要的 determineDataSource 還是個抽象方法,那沒辦法了,看它有哪些子類實現
DynamicRoutingDataSource 的 determineDataSource 方法如下
DynamicDataSourceContextHolder 有沒有感覺到熟悉?
想想它的 ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER ,回憶上來了沒?
出棧,獲取到當前的資料來源名;接下來該分析誰了?
那肯定是 getDataSource 方法
1、如果資料來源為空,那麼直接返回預設資料來源,對應配置檔案中的
2、分組資料來源,我們的示例程式碼那麼簡單,應該沒涉及到這個,先不管
3、所有資料來源,是一個 LinkHashMap ,key 是 資料來源名 ,value 是資料來源
可想而知,我們示例的資料來源獲取就是從該 map 獲取的
4、是否啟用嚴格模式,預設不啟動。嚴格模式下未匹配到資料來源直接報錯,,非嚴格模式下則使用預設資料來源 primary 所設定的資料來源
5、對應 4,未開啟嚴格模式,未匹配到資料來源則使用 primary 所設定的資料來源
那現在又該分析誰?肯定是 dataSourceMap 的值是怎麼 put 進去的
我們看哪些地方用到了 dataSourceMap
發現就一個地方進行了 put
那這個 addDataSource 方法又在哪被呼叫了?
DynamicRoutingDataSource 實現了 InitializingBean ,所以在啟動過程中,它的 afterPropertiesSet 方法會被呼叫,至於為什麼,大家自行去查閱
接下來該分析什麼?那肯定是 Map<String, DataSource> dataSources = provider.loadDataSources();
我們跟進 loadDataSources() ,發現有兩個類都有該方法
那麼我們應該跟誰?有兩種方法
1、憑感覺,我們的配置檔案是 yml
2、打斷點,重新啟動專案,一目瞭然
YmlDynamicDataSourceProvider 的 loadDataSources 方法如下
(這裡留個疑問: dataSourcePropertiesMap 存放的是什麼,值是如何 put 進去的?)
繼續往下跟 createDataSourceMap 方法
1、配置檔案中的資料來源屬性,斷點下就很清楚了
2、根據資料來源屬性建立資料來源,然後放進 dataSourceMap 中
建立資料來源的過程就不跟了,感興趣的自行去研究
至此,不知道大家清楚了沒? 我反正是暈了
總結
1、萬變不離其宗,多資料來源的原理是不變的
原理解密 → Spring AOP 實現動態資料來源(讀寫分離),底層原理是什麼
2、苞米豆的多資料來源的自動配置類
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
這個配置類很重要,很多重要的物件都是在這裡注入到 Spring 容器中的
關於自動配置,大家可參考:springboot2.0.3原始碼篇 - 自動配置的實現,發現也不是那麼複雜
3、遇到問題,不要立馬一頭扎進去,自己實現,多查查,看是否有現成的第三方實現
自己實現,很容易踩別人踩過的坑,容易浪費時間;另外侷限性太大,不易擴充,畢竟一人之力有限