1. 程式人生 > 其它 >spring boot2.0 +Mybatis + druid搭建一個最簡單的多資料來源

spring boot2.0 +Mybatis + druid搭建一個最簡單的多資料來源

多資料來源系列
1、spring boot2.0 +Mybatis + druid搭建一個最簡單的多資料來源
2、利用Spring的AbstractRoutingDataSource做多資料來源動態切換
3、使用dynamic-datasource-spring-boot-starter做多資料來源及原始碼分析

簡介
前兩篇部落格介紹了用基本的方式做多資料來源,可以應對一般的情況,但是遇到一些複雜的情況就需要擴充套件下功能了,比如:動態增減資料來源、資料來源分組,純粹多庫 讀寫分離 一主多從、從其他資料庫或者配置中心讀取資料來源等等。其實就算沒有這些需求,使用這個實現多資料來源也比之前使用AbstractRoutingDataSource要便捷的多

dynamic-datasource-spring-boot-starter 是一個基於springboot的快速整合多資料來源的啟動器。
github: https://github.com/baomidou/dynamic-datasource-spring-boot-starter
文件: https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki

它跟mybatis-plus是一個生態圈裡的,很容易整合mybatis-plus

特性:

  • 資料來源分組,適用於多種場景 純粹多庫 讀寫分離 一主多從 混合模式。
  • 內建敏感引數加密和啟動初始化表結構schema資料庫database。
  • 提供對Druid,Mybatis-Plus,P6sy,Jndi的快速整合。
  • 簡化Druid和HikariCp配置,提供全域性引數配置。
  • 提供自定義資料來源來源介面(預設使用yml或properties配置)。
  • 提供專案啟動後增減資料來源方案。
  • 提供Mybatis環境下的 純讀寫分離 方案。
  • 使用spel動態引數解析資料來源,如從session,header或引數中獲取資料來源。(多租戶架構神器)
  • 提供多層資料來源巢狀切換。(ServiceA >>> ServiceB >>> ServiceC,每個Service都是不同的資料來源)
  • 提供 不使用註解 而 使用 正則 或 spel 來切換資料來源方案(實驗性功能)。
  • 基於seata的分散式事務支援。

實操
先把座標丟出來

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.1.0</version>
</dependency>

下面抽幾個用的比較多的應用場景介紹

基本使用

使用方法很簡潔,分兩步走
一:通過yml配置好資料來源
二:service層裡面在想要切換資料來源的方法上加上@DS註解就行了,也可以加在整個service層上,方法上的註解優先於類上註解

spring:
  datasource:
    dynamic:
      primary: master #設定預設的資料來源或者資料來源組,預設值即為master
      strict: false #設定嚴格模式,預設false不啟動. 啟動後在未匹配到指定資料來源時候回丟擲異常,不啟動會使用預設資料來源.
      datasource:
        master:
          url: jdbc:mysql://127.0.0.1:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        db1:
          url: jdbc:gbase://127.0.0.1:5258/dynamic
          username: root
          password: 123456
          driver-class-name: com.gbase.jdbc.Driver

這就是兩個不同資料來源的配置,接下來寫service程式碼就行了

# 多主多從
spring:
  datasource:
    dynamic:
      datasource:
        master_1:
        master_2:
        slave_1: 
        slave_2: 
        slave_3:   

如果是多主多從,那麼就用資料組名稱_xxx,下劃線前面的就是資料組名稱,相同組名稱的資料來源會放在一個組下。切換資料來源時,可以指定具體資料來源名稱,也可以指定組名然後會自動採用負載均衡演算法切換

# 純粹多庫(記得設定primary)
spring:
  datasource:
    dynamic:
      datasource:
        db1:
        db2:
        db3: 
        db4: 
        db5:  

純粹多庫,就一個一個往上加就行了

@Service
@DS("master")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List<Map<String, Object>> selectAll() {
    return jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("db1")
  public List<Map<String, Object>> selectByCondition() {
    return jdbcTemplate.queryForList("select * from user where age >10");
  }
}
註解結果
沒有@DS 預設資料來源
@DS(“dsName”) dsName可以為組名也可以為具體某個庫的名稱


通過日誌可以發現我們配置的多資料來源已經被初始化了,如果切換資料來源也會看到列印日子的
是不是很便捷,這是官方的例子

整合druid連線池

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>

首先引入依賴

spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

再排除掉druid原生的自動配置

spring:
  datasource: #資料庫連結相關配置
    dynamic:
      druid: #以下是全域性預設值,可以全域性更改
        #監控統計攔截的filters
        filters: stat
        #配置初始化大小/最小/最大
        initial-size: 1
        min-idle: 1
        max-active: 20
        #獲取連線等待超時時間
        max-wait: 60000
        #間隔多久進行一次檢測,檢測需要關閉的空閒連線
        time-between-eviction-runs-millis: 60000
        #一個連線在池中最小生存的時間
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT 'x'
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        #開啟PSCache,並指定每個連線上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設定為false
        pool-prepared-statements: false
        max-pool-prepared-statement-per-connection-size: 20
        stat:
          merge-sql: true
          log-slow-sql: true
          slow-sql-millis: 2000
            primary: master
      datasource:
        master:
          url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
        gbase1:
          url: jdbc:gbase://127.0.0.1:5258/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull
          username: gbase
          password: gbase
          driver-class-name: com.gbase.jdbc.Driver
          druid: # 以下引數針對每個庫可以重新設定druid引數
            initial-size:
            validation-query: select 1 FROM DUAL #比如oracle就需要重新設定這個
            public-key: #(非全域性引數)設定即表示啟用加密,底層會自動幫你配置相關的連線引數和filter。

配置好了就可以了,切換資料來源的用法和上面的一樣的,打@DS(“db1”)註解到service類或方法上就行了
詳細配置參考這個配置類com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties

service巢狀
這個就是特性的第九條:提供多層資料來源巢狀切換。(ServiceA >>> ServiceB >>> ServiceC,每個Service都是不同的資料來源)
借用原始碼中的demo:實現SchoolService >>> studentService、teacherService

@Service
public class SchoolServiceImpl{
    public void addTeacherAndStudent() {
        teacherService.addTeacherWithTx("ss", 1);
        teacherMapper.addTeacher("test", 111);
        studentService.addStudentWithTx("tt", 2);
    }
}
@Service
@DS("teacher")
public class TeacherServiceImpl {
    public boolean addTeacherWithTx(String name, Integer age) {
        return teacherMapper.addTeacher(name, age);
    }
}
@Service
@DS("student")
public class StudentServiceImpl {
    public boolean addStudentWithTx(String name, Integer age) {
        return studentMapper.addStudent(name, age);
    }
}

這個addTeacherAndStudent呼叫資料來源切換就是primary ->teacher->primary->student->primary


關於其他demo可以看官方wiki,裡面寫了很多用法,這裡就不贅述了,重點在於學習原理。。。

為什麼切換資料來源不生效或事務不生效?
這種問題常見於上一節service巢狀,比如serviceA -> serviceB、serviceC,serviceA
加上@Transaction

簡單來說:巢狀資料來源的service中,如果操作了多個數據源,不能在最外層加上@Transaction開啟事務,否則切換資料來源不生效,因為這屬於分散式事務了,需要用seata方案解決,如果是單個數據源(不需要切換資料來源)可以用@Transaction開啟事務,保證每個資料來源自己的完整性

下面來粗略的分析加事務不生效的原因:
它這個切換資料來源的原理就是實現了DataSource介面,實現了getConnection方法,只要在service中開啟事務,service中對其他資料來源操作只會使用開啟事務的資料來源,因為開啟事務資料來源會被快取下來,可以在DataSourceTransactionManager的doBegin方法中看見那個txObject,如果在一個事務內,就會複用Connection,所以切換不了資料來源

    /**
     * This implementation sets the isolation level but ignores the timeout.
     */
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;

        try {
            if (!txObject.hasConnectionHolder() ||
                    txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                // 開啟一個新事務會獲取一個新的Connection,所以會呼叫DataSource介面的getConnection方法,從而切換資料來源
                Connection newCon = obtainDataSource().getConnection();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }
                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            // 如果已經開啟了事務,就從holder中獲取Connection
            con = txObject.getConnectionHolder().getConnection();
            …………
            }

多資料來源事務巢狀
看上面原始碼,說是新起一個事務才會重新獲取Connection,才會成功切換資料來源,那我在每個資料來源的service方法上都加上@Transaction呢?(涉及spring事務傳播行為)這裡做個小實驗,還是上面的例子,serviceA ->(巢狀) serviceB、serviceC,serviceA

加上@Transaction,現在給serviceB和serviceC的方法上也加上@Transaction,就是所有service裡被呼叫的方法都打上@Transaction註解

@Transactional
public void addTeacherAndStudentWithTx() {
    teacherService.addTeacherWithTx("ss", 1);
    studentService.addStudentWithTx("tt", 2);
    throw new RuntimeException("test");
}

類似這樣,裡面兩個service也都加上了@Transaction

實際上這樣資料來源也不會切換,因為預設事務傳播級別為required,父子service屬於同一事物所以就會用同一Connection。而這裡是多資料來源,如果把事務傳播方式改成require_new給子service起新事物,可以切換資料來源,他們都是獨立的事務了,然後父service回滾不會導致子service回滾(詳見spring事務傳播),這樣保證了每個單獨的資料來源的資料完整性,如果要保證所有資料來源的完整性,那就用seata分散式事務框架

@Transactional
public void addTeacherAndStudentWithTx() {
    // 做了資料庫操作
    aaaDao.doSomethings(“test”);
    teacherService.addTeacherWithTx("ss", 1);
    studentService.addStudentWithTx("tt", 2);
    throw new RuntimeException("test");
}

關於事務巢狀,還有一種情況就是在外部service裡面做DB1的一些操作,然後再呼叫DB2、DB3的service,再想保證DB1的事務,就需要在外部service上加@Transaction,如果想讓裡面的service正常切換資料來源,根據事務傳播行為,設定為propagation = Propagation.REQUIRES_NEW就可以了,裡面的也能正常切換資料來源了,因為它們是獨立的事務

補充:關於@Transaction操作多資料來源事務的問題