多數據源系列
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操作多數據源事務的問題
@Transaction public void insertDB1andDB2() { db1Service.insertOne(); db2Service.insertOne(); throw new RuntimeException("test"); }
類似於上面這種操作,我們通過注入多個DataSource、DataSourceTransactionManager、SqlSessionFactory、SqlSessionTemplate這四種Bean的方式來實現多數據源(最頂上第一篇博客提到的方式),然后在外部又加上了@Transaction想實現事務
我試過在中間拋異常查看能不能正常回滾,結果發現只會有一個數據源的事務生效,點開@Transaction注解,發現里面有個transactionManager屬性,這個就是指定之前聲明的transactionManager Bean,我們默認了DB1的transactionManager為@Primary,所以這時DB2的事務就不會生效,因為用的是DB1的TransactionManager。因為@Transactional只能指定一個事務管理器,並且注解不允許重復,所以就只能使用一個數據源的事務管理器了。如果DB2中的更新失敗,我想回滾DB1和DB2以進行回滾,可以使用ChainedTransactionManager來解決,它可以最后盡最大努力回滾事務