苞米豆的多資料來源 → dynamic-datasource-spring-boot-starter,挺香的!

2023-04-21 12:01:41

開心一刻

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

  媽:你到底想多少歲結婚

  我:60

  媽:60,你想找個多大的

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

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

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

需求背景

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

  一個資料來源是 MySQL ,另一個資料來源是 SQL Server 

  樓主是一點都不慌的,因為我寫過好幾篇關於資料來源的文章

    spring整合mybatis實現mysql讀寫分離

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

    Spring 下,關於動態資料來源的事務問題的探討

  我會慌?

  直接一發入魂,眼前一黑,不對,是眼前一亮!

  感覺就是它了!

MyBatis-Plus 多資料來源

  關於苞米豆(baomidou),我們最熟悉的肯定是 MyBatis-Plus 

  但旗下還有很多其他優秀的元件

  多資料來源就是其中一個,今天我們就來會會它

  資料來源準備

  用 docker 準備一個 MySQL 和 SQL Server ,圖省事,兩個資料庫伺服器放到同個 docker 下了

  有小夥伴會覺得放一起不合適,有單點問題!

  樓主只是為了演示,糾結那麼細,當心敲你狗頭

  建庫: 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("資料來源名稱") 即可,例如

  我們來看下效果

  難道一個 @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 條在上面已經分析過了把,是不是?你回答是就完事了

  那我們就找 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、遇到問題,不要立馬一頭扎進去,自己實現,多查查,看是否有現成的第三方實現

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