背景
使用mysql的代理中間件,某些接口如果主從同步延遲大,容易出現邏輯問題。所以程序中沒有直接使用這個中間件。
依賴程序邏輯,如果有一些接口可以走讀庫,需要一個可以顯示指定讀庫的方式來連接讀庫,降低主庫的壓力。
所以需要一個能配置多個數據源,通過注解或者代碼方式設置讀還是寫數據源的方法。
實現方式
使用MybatisPlus的多數據源切換的能力。
github地址: https://github.com/baomidou/dynamic-datasource
已經封裝好了。直接使用就行。
介紹
dynamic-datasource-spring-boot-starter 是一個基于springboot的快速集成多數據源的啟動器。
其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x。
JPA用戶不建議使用,JPA自帶事務,無法連續切庫。
特性:
- 支持 數據源分組 ,適用于多種場景 純粹多庫 讀寫分離 一主多從 混合模式。
- 支持數據庫敏感配置信息 加密(可自定義) ENC()。
- 支持每個數據庫獨立初始化表結構schema和數據庫database。
- 支持無數據源啟動,支持懶加載數據源(需要的時候再創建連接)。
- 支持 自定義注解 ,需繼承DS(3.2.0+)。
- 提供并簡化對Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供對Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等組件的集成方案。
- 提供 自定義數據源來源 方案(如全從數據庫加載)。
- 提供項目啟動后 動態增加移除數據源 方案。
- 提供Mybatis環境下的 純讀寫分離 方案。
- 提供使用 spel動態參數 解析數據源方案。內置spel,session,header,支持自定義。
- 支持 多層數據源嵌套切換 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 基于seata的分布式事務方案 。
- 提供 本地多數據源事務方案。
我們需要的是純讀寫分離 方案,它已經內置了。
功能范圍:
- 本框架只做 切換數據源 這件核心的事情,并不限制你的具體操作,切換了數據源可以做任何CRUD。
- 配置文件所有以下劃線 分割的數據源 首部 即為組的名稱,相同組名稱的數據源會放在一個組下。
- 切換數據源可以是組名,也可以是具體數據源名稱。組名則切換時采用負載均衡算法切換。
- 默認的數據源名稱為 master ,你可以通過 spring.datasource.dynamic.primary 修改。
- 方法上的注解優先于類上注解。
- DS支持繼承抽象類上的DS,暫不支持繼承接口上的DS。
使用步驟
1.引入依賴。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
2.配置數據源
spring:
datasource:
dynamic:
primary: master #設置默認的數據源或者數據源組,默認值即為master
strict: false #嚴格匹配數據源,默認false. true未匹配到指定數據源時拋異常,false使用默認數據源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0開始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 內置加密,使用請查看詳細文檔
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上會配置一個默認庫master,一個組slave下有兩個子庫slave_1,slave_2
3.使用注解切換數據源。
@DS 可以注解在方法上或類上,同時存在就近原則 方法上注解 優先于 類上注解。
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
業務集成
我們的程序使用的經典的三層結構,即MVC .
看到我們的框架是已經集成了dynamicSource的。如果沒有,則加一下依賴即可。
以crm服務為例子。寫了一些測試代碼,進行驗證測試。
package com.pig4cloud.pigx.crm.controller;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.crm.mapper.AiAgentMapper;
import com.pig4cloud.pigx.crm.service.AiAgentService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequiredArgsConstructor
@Tag(description = "test read write", name = "測試讀寫分離")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
@Valid
@Slf4j
public class TestDsController {
private final AiAgentMapper aiAgentMapper;
private final AiAgentService aiAgentService;
@GetMapping("/testD")
@ResponseBody
//@Inner(value = false)
public R testD() {
return R.ok(aiAgentMapper.selectList(null));
}
@GetMapping("/testRead")
@ResponseBody
//@Inner(value = false)
@DS("read")
public R testRead() {
return R.ok(aiAgentMapper.selectList(null));
}
@GetMapping("/testMaster")
@ResponseBody
//@Inner(value = false)
@DS("master")
public R testMaster() {
return R.ok(aiAgentMapper.selectList(null));
}
@GetMapping("/testHe")
@ResponseBody
//@Inner(value = false)
@DS("master")
public R testHe() {
return R.ok(aiAgentService.listTest());
}
@GetMapping("/testHe2")
@ResponseBody
//@Inner(value = false)
@DS("read")
public R testHe2() {
return R.ok(aiAgentService.listTest2());
}
@GetMapping(value = "/testHe3",produces = "application/json")
@ResponseBody
//@Inner(value = false)
public R testHe3() {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("read", aiAgentService.listTest());
dataMap.put("master", aiAgentService.listTest2());
return R.ok(dataMap);
}
}
對應的Service實現代碼:
@Service
@Slf4j
@RequiredArgsConstructor
public class AiAgentServiceImpl extends ServiceImpl<AiAgentMapper, AiAgentEntity> implements AiAgentService {
@Override
@DS("read")
public List<AiAgentEntity> listTest() {
return baseMapper.selectList(null);
}
@Override
@DS("master")
public List<AiAgentEntity> listTest2() {
return baseMapper.selectList(null);
}
}
測試用例和結果:
測試接口和場景 | 測試說明 | 預期結果 | 測試結果 |
---|---|---|---|
/testD | 不加任何注解 | 走主庫 | - [x] 走主庫 ![] file
|
/testRead | 添加了讀庫注解 | 走讀庫 | - [x] 走讀庫 file
|
/testMaster | 添加了主庫注解 | 走主庫 | - [x] 走主庫 file
|
/testHe | 上層加主庫注解,下層加讀庫注解 | 走讀庫 | - [x] 走讀庫 |
/testHe2 | 上層加讀庫注解,下層加寫庫注解 | 走主庫 | - [x] 走主庫 |
/testHe3 | 在中層的方法分別加讀庫和寫庫注解 | 分別走讀庫和主庫 | - [x] 分別走讀庫和讀庫 file
|
目前需要增加讀寫分離的服務。
服務 | 增加讀寫分離的原因 | 是否完成增加 |
---|---|---|
crm | 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; | - [x] |
后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; | - [ ] | |
phone | 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; | - [ ] |
ssm | 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; | - [ ] |
upms | 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; | - [ ] |
源碼研究
3.6.0版本。基于這個進行分析。
1 自動裝配類
配置屬性:DynamicDataSourceProperties
屬性 | 說明 |
---|---|
primary | 默認的庫 |
strict | 嚴格檢查 |
p6spy | 日志輸出 |
seata | 是否開啟seata |
lazy | 是否懶加載數據源 |
setaMode | AT模式 |
<font style="color:#9876aa;background-color:#2b2b2b;">publicKey | 加密key |
datasource | 數據源,可以單獨配置; |
strategy | 負載均衡策略 |
druid | 數據源 |
hikari | 數據源 |
beecp | 數據源 |
dbcp2 | 數據源 |
aop | aop配置,有默認值 |
DataSourceProperty
屬性說明 | 說明 |
---|---|
poolName | 連接池名稱 |
type | 連接類型 |
driverClassName | 驅動名稱 |
url | 連接url |
username | 用戶名 |
password | 密碼 |
jndiName | jndi中的名稱 |
init | 初始化配置 |
p6spy | 日志輸出 |
seata | 是否開啟seata |
lazy | 是否懶加載數據源 |
<font style="color:#9876aa;background-color:#2b2b2b;">publicKey | 加密key |
druid | 數據源 |
hikari | 數據源 |
beecp | 數據源 |
dbcp2 | 數據源 |
DatasourceInitProperties
屬性名 | 說明 |
---|---|
schema | 建表腳本 |
data | 數據腳本 |
<font style="color:#9876aa;background-color:#2b2b2b;">continueOnError | 錯誤是否繼續 |
<font style="color:#9876aa;background-color:#2b2b2b;">separator | 分隔符 |
2 自動裝配邏輯
基于切面實現。
小結
集成使用了mybatisPlus自帶的多數據源實現了讀寫分離,并探究了源碼實現過程。
詳細過程可以看我的視頻號。
原創不易,關注誠可貴,轉發價更高!轉載請注明出處,讓我們互通有無,共同進步,歡迎溝通交流。
我會持續分享Java軟件編程知識和程序員發展職業之路,歡迎關注!