苞米豆的多數據源 → dynamic-datasource-spring-boot-starter,挺香的!

開心一刻

  2023年元旦,我媽又開始了對我的唸叨

  媽:你到底想多少歲結婚

  我:60

  媽:60,你想找個多大的

  我:找個55的啊,她55我60,結婚都有退休金,不用上班不用生孩子,不用買車買房,成天就是玩兒

  我:而且一結婚就是白頭偕老,多好

  我媽直接一大嘴巴子呼我臉上

需求背景

  最近接到一個需求,需要從兩個數據源獲取數據,然後進行彙總展示

  一個數據源是 MySQL ,另一個數據源是 SQL Server 

  樓主是一點都不慌的,因爲我寫過好幾篇關於數據源的文章

    spring集成mybatis實現mysql讀寫分離

    原理解密 → Spring AOP 實現動態數據源(讀寫分離),底層原理是什麼

    Spring 下,關於動態數據源的事務問題的探討

  我會慌?

  但還是有點小拒絕,爲什麼了?

  自己實現的話,要寫的東西還是很多,要寫 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('張三'),('李四');
View Code

   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');
View Code

  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>
View Code

  配置也很簡單, 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
View Code

  然後在對應的類或者方法上加上註解 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、遇到問題,不要立馬一頭扎進去,自己實現,多查查,看是否有現成的第三方實現

    自己實現,很容易踩別人踩過的坑,容易浪費時間;另外侷限性太大,不易拓展,畢竟一人之力有限

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