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操作多資料來源事務的問題