SpringBoot+MyBatis-Plus+Dynamic-Datasource讀寫分離完整指南
在SpringBoot項目中,使用MyBatis-Plus和Dynamic-Datasource實現讀寫分離是一種常見且高效的架構選擇。下面我將為你詳細講解完整的實現方案,包括環(huán)境準備、配置、代碼實現以及注意事項。
1. 讀寫分離概述與核心概念
讀寫分離是一種常見的數據庫優(yōu)化方案,其核心思想是將數據庫的寫操作(INSERT、UPDATE、DELETE)和讀操作(SELECT)分發(fā)到不同的數據庫節(jié)點上。主數據庫(Master)負責處理所有寫操作,而從數據庫(Slave)負責處理讀操作,通過數據庫的主從復制機制保持數據同步。
1.1 核心價值
- 提升并發(fā)性能:將讀請求分散到多個從庫,減輕主庫壓力
- 提高系統(tǒng)可用性:單個從庫故障不影響讀服務
- 優(yōu)化響應速度:專庫專用,避免讀寫操作相互阻塞
1.2 技術架構
應用程序 → Dynamic-Datasource → 主庫(寫操作)
↓
從庫(讀操作)
2. 環(huán)境準備與依賴配置
2.1 添加Maven依賴
首先在項目的pom.xml中添加必要的依賴:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 啟動器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- Dynamic-Datasource 啟動器(核心依賴) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- MySQL 驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 連接池(可選,HikariCP已內置) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
2.2 配置文件設置
在application.yml中配置主從數據源:
spring:
datasource:
dynamic:
primary: master # 設置默認數據源為主庫
strict: false # 是否嚴格匹配數據源,false時未匹配到指定數據源則使用默認數據源
datasource:
# 主庫配置(寫操作)
master:
url: jdbc:mysql://master-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: admin
password: master@123
driver-class-name: com.mysql.cj.jdbc.Driver
# 從庫配置(讀操作)- 支持多個從庫
slave1:
url: jdbc:mysql://slave1-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: readonly
password: slave@123
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
url: jdbc:mysql://slave2-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: readonly
password: slave@123
driver-class-name: com.mysql.cj.jdbc.Driver
# 負載均衡策略配置(多個從庫時生效)
strategy:
slave: round_robin # 從庫負載均衡策略:random(隨機)/round_robin(輪詢)
# 連接池配置(HikariCP)
hikari:
max-pool-size: 20 # 最大連接數
min-idle: 5 # 最小空閑連接
connection-timeout: 30000 # 連接超時時間(ms)
idle-timeout: 600000 # 空閑連接超時時間(ms)
max-lifetime: 1800000 # 連接最大生命周期(ms)
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 開啟駝峰命名轉換
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(調試用)
global-config:
db-config:
id-type: auto # 主鍵策略自增
3. 數據源與MyBatis-Plus配置類
3.1 動態(tài)數據源自動配置
Dynamic-Datasource starter已經提供了自動配置,大多數情況下無需額外配置。但如果需要自定義,可以創(chuàng)建配置類:
@Configuration
@MapperScan("com.example.mapper") // 指定MyBatis mapper接口的掃描路徑
public class DataSourceConfig {
/**
* 配置動態(tài)數據源
* Dynamic-Datasource會自動根據application.yml的配置創(chuàng)建數據源
* 此處可以添加自定義配置
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dynamic")
public DynamicDataSourceProperties dynamicDataSourceProperties() {
return new DynamicDataSourceProperties();
}
/**
* 配置MyBatis-Plus攔截器(分頁插件等)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分頁插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L); // 設置最大分頁限制
interceptor.addInnerInterceptor(paginationInterceptor);
// 可以添加其他插件,如樂觀鎖插件等
return interceptor;
}
}
4. 業(yè)務層實現讀寫分離
4.1 使用@DS注解手動切換數據源
最直接的方式是在Service層使用@DS注解指定數據源:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 寫操作:使用@DS("master")指定主庫
* 注意:寫操作建議都使用主庫,確保數據一致性
*/
@DS("master") // 指定使用主庫
@Transactional(rollbackFor = Exception.class) // 添加事務管理
@Override
public boolean createUser(User user) {
// 參數校驗
if (user == null || StringUtils.isBlank(user.getUsername())) {
throw new IllegalArgumentException("用戶信息不完整");
}
// 設置創(chuàng)建時間
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 執(zhí)行插入操作,這里會使用主庫
boolean result = save(user);
// 這里可以添加其他業(yè)務邏輯
log.info("創(chuàng)建用戶成功,用戶ID: {}", user.getId());
return result;
}
/**
* 讀操作:使用@DS("slave")指定從庫
* 框架會根據負載均衡策略選擇slave1或slave2
*/
@DS("slave") // 指定使用從庫(負載均衡)
@Override
public User getUserById(Long id) {
// 參數校驗
if (id == null || id <= 0) {
throw new IllegalArgumentException("用戶ID不合法");
}
// 查詢用戶信息,這里會使用從庫
User user = getById(id);
if (user == null) {
log.warn("未找到對應用戶,用戶ID: {}", id);
throw new RuntimeException("用戶不存在");
}
return user;
}
/**
* 批量查詢:同樣使用從庫
*/
@DS("slave")
@Override
public List<User> listUsersByCondition(UserQuery query) {
// 構建查詢條件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(query.getUsername())) {
wrapper.like(User::getUsername, query.getUsername());
}
if (query.getStatus() != null) {
wrapper.eq(User::getStatus, query.getStatus());
}
// 添加排序
wrapper.orderByDesc(User::getCreateTime);
// 執(zhí)行查詢,使用從庫
return list(wrapper);
}
/**
* 更新操作:必須使用主庫
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
@Override
public boolean updateUser(User user) {
if (user == null || user.getId() == null) {
throw new IllegalArgumentException("用戶信息不完整");
}
// 設置更新時間
user.setUpdateTime(LocalDateTime.now());
// 執(zhí)行更新,使用主庫
boolean result = updateById(user);
if (result) {
log.info("更新用戶成功,用戶ID: {}", user.getId());
} else {
log.error("更新用戶失敗,用戶ID: {}", user.getId());
}
return result;
}
/**
* 刪除操作:必須使用主庫
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
@Override
public boolean deleteUser(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("用戶ID不合法");
}
// 執(zhí)行刪除,使用主庫
boolean result = removeById(id);
if (result) {
log.info("刪除用戶成功,用戶ID: {}", id);
} else {
log.warn("刪除用戶失敗,用戶ID: {}", id);
}
return result;
}
}
4.2 基于AOP的自動數據源切換(推薦)
對于大型項目,手動添加@DS注解比較繁瑣,可以通過AOP自動根據方法類型切換數據源:
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
/**
* 定義切點:攔截Service層的所有方法
*/
@Pointcut("execution(* com.example.service..*.*(..))")
public void servicePointcut() {}
/**
* 前置通知:在方法執(zhí)行前選擇數據源
*/
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
// 獲取方法名
String methodName = joinPoint.getSignature().getName();
// 根據方法名前綴判斷是讀操作還是寫操作
if (isReadOperation(methodName)) {
// 讀操作使用從庫
DynamicDataSourceContextHolder.push("slave");
log.debug("切換數據源到從庫,方法名: {}", methodName);
} else {
// 寫操作使用主庫
DynamicDataSourceContextHolder.push("master");
log.debug("切換數據源到主庫,方法名: {}", methodName);
}
}
/**
* 后置通知:清理數據源上下文
*/
@After("servicePointcut()")
public void after() {
DynamicDataSourceContextHolder.clear();
log.debug("清除數據源上下文");
}
/**
* 判斷是否為讀操作
* 可以根據方法名前綴進行判斷
*/
private boolean isReadOperation(String methodName) {
// 常見的讀操作方法前綴
String[] readPrefixes = {"get", "select", "list", "query", "find", "search", "count", "check"};
for (String prefix : readPrefixes) {
if (methodName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
使用AOP后,Service層的代碼可以簡化為:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Transactional(rollbackFor = Exception.class)
@Override
public boolean createUser(User user) {
// 自動使用主庫(AOP根據方法名判斷)
user.setCreateTime(LocalDateTime.now());
return save(user);
}
@Override
public User getUserById(Long id) {
// 自動使用從庫(AOP根據方法名前綴"get"判斷)
return getById(id);
}
// 其他方法無需添加@DS注解,由AOP自動處理
}
5. 事務處理策略
事務處理是讀寫分離中的關鍵問題,需要特別注意:
5.1 單數據源事務
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
/**
* 單數據源事務:所有操作都在主庫執(zhí)行
* 使用@DS("master")確保即使有AOP配置也使用主庫
*/
@DS("master") // 顯式指定主庫
@Transactional(rollbackFor = Exception.class) // 聲明式事務
@Override
public void createOrder(Order order) {
// 1. 保存訂單主信息(主庫)
save(order);
// 2. 更新庫存(主庫)
updateStock(order);
// 3. 記錄操作日志(主庫)
logOrderOperation(order);
// 如果任何一步失敗,整個事務回滾
}
/**
* 只讀事務:可以指定從庫
*/
@DS("slave")
@Transactional(readOnly = true) // 只讀事務,有優(yōu)化效果
@Override
public Order getOrderDetail(Long orderId) {
return getById(orderId);
}
}
5.2 多數據源事務處理
對于涉及多個數據源的復雜事務,需要使用分布式事務解決方案:
@Service
@Slf4j
public class DistributedTransactionService {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
/**
* 使用@DSTransactional實現多數據源分布式事務
* 注意:需要集成Seata等分布式事務框架
*/
// @DSTransactional // 分布式事務注解(需要額外配置)
@Transactional(rollbackFor = Exception.class)
public void placeOrder(Order order, User user) {
try {
// 1. 用戶操作(主庫)
userService.updateUser(user);
// 2. 訂單操作(主庫)
orderService.createOrder(order);
// 模擬業(yè)務異常
if (order.getAmount() == null) {
throw new RuntimeException("訂單金額不能為空");
}
log.info("下單成功");
} catch (Exception e) {
log.error("下單失敗,已回滾", e);
throw e; // 拋出異常觸發(fā)回滾
}
}
}
6. 高級功能與優(yōu)化策略
6.1 多從庫負載均衡
當配置多個從庫時,Dynamic-Datasource支持多種負載均衡策略:
spring:
datasource:
dynamic:
datasource:
master:
# 主庫配置...
slave1:
# 從庫1配置...
slave2:
# 從庫2配置...
slave3:
# 從庫3配置...
strategy:
slave: round_robin # 負載均衡策略
# 可選值:
# random - 隨機選擇(默認)
# round_robin - 輪詢
# weight_round_robin - 加權輪詢(需要額外配置權重)
負載均衡策略對比:
| 策略類型 | 描述 | 適用場景 |
|---|---|---|
| random | 隨機選擇從庫 | 簡單的讀負載均衡 |
| round_robin | 輪詢選擇從庫 | 從庫配置相近,需要均勻分布 |
| weight_round_robin | 根據權重選擇 | 從庫配置不同,按性能分配負載 |
6.2 健康檢查與故障轉移
@Component
@Slf4j
public class DataSourceHealthCheck {
@Autowired
private DataSource dataSource;
/**
* 定時檢查從庫健康狀態(tài)
*/
@Scheduled(fixedRate = 60000) // 每分鐘執(zhí)行一次
public void checkSlaveHealth() {
// 這里可以實現從庫健康檢查邏輯
// 如果某個從庫不可用,可以動態(tài)將其從負載均衡池中移除
log.info("執(zhí)行從庫健康檢查...");
// 實際實現中可以使用JDBC測試連接或查詢數據庫狀態(tài)
}
/**
* 獲取從庫同步延遲
*/
public Long getReplicationDelay(String slaveName) {
// 執(zhí)行SQL查詢從庫同步狀態(tài)
// SHOW SLAVE STATUS 可以獲取Seconds_Behind_Master等信息
// 如果延遲過大,可以臨時將該從庫標記為不可用
return 0L; // 返回延遲秒數
}
}
7. 測試與驗證
7.1 單元測試
@SpringBootTest
@Slf4j
class ReadWriteSeparationTest {
@Autowired
private UserService userService;
/**
* 測試寫操作(應該路由到主庫)
*/
@Test
void testWriteOperation() {
User user = new User();
user.setUsername("testUser");
user.setEmail("test@example.com");
boolean result = userService.createUser(user);
assertTrue(result);
log.info("寫操作測試通過,應路由到主庫");
}
/**
* 測試讀操作(應該路由到從庫)
*/
@Test
void testReadOperation() {
User user = userService.getUserById(1L);
assertNotNull(user);
log.info("讀操作測試通過,應路由到從庫");
}
/**
* 測試事務內的數據源選擇
*/
@Test
void testTransactionalOperation() {
// 測試事務方法,應始終使用主庫
User user = userService.getUserById(1L);
assertNotNull(user);
log.info("事務內操作測試完成");
}
}
7.2 驗證數據源路由
在application.yml中開啟SQL日志,驗證讀寫分離是否生效:
# 開啟MyBatis SQL日志
logging:
level:
com.example.mapper: debug # Mapper接口的包路徑
com.baomidou.dynamic.datasource: debug # Dynamic-Datasource日志
觀察控制臺輸出,應該能看到類似日志:
2025-11-11 10:00:00 | Master DataSource | INSERT INTO user ... 2025-11-11 10:00:05 | Slave DataSource | SELECT * FROM user ...
8. 生產環(huán)境注意事項
8.1 主從同步延遲處理
@Service
public class CriticalReadService {
@Autowired
private UserMapper userMapper;
/**
* 對于一致性要求高的讀操作,強制走主庫
*/
@DS("master") // 強制使用主庫,避免讀取舊數據
public User getCriticalUserInfo(Long userId) {
// 例如:賬戶余額、訂單狀態(tài)等關鍵信息
return userMapper.selectById(userId);
}
/**
* 檢查數據是否已同步
*/
public boolean waitForReplication(Long userId, int maxWaitSeconds) {
for (int i = 0; i < maxWaitSeconds; i++) {
User masterUser = getFromMaster(userId);
User slaveUser = getFromSlave(userId);
if (Objects.equals(masterUser, slaveUser)) {
return true; // 數據已同步
}
try {
Thread.sleep(1000); // 等待1秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return false; // 同步超時
}
}
8.2 監(jiān)控與告警
# 連接池監(jiān)控配置
spring:
datasource:
dynamic:
druid:
# 開啟監(jiān)控統(tǒng)計
filters: stat,wall,log4j
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin
9. 常見問題與解決方案
9.1 事務內數據源切換失效
問題:在@Transactional方法內,數據源在方法開始時確定,方法內部調用無法切換數據源。
解決方案:
@Service
public class TransactionalService {
@Autowired
private ApplicationContext applicationContext;
@Transactional
public void transactionalMethod() {
// 方法內需要切換數據源時,通過代理對象調用
TransactionalService self = applicationContext.getBean(TransactionalService.class);
// 通過代理對象調用,可以正常切換數據源
self.readOperation(); // 使用從庫
self.writeOperation(); // 使用主庫
}
@DS("slave")
public void readOperation() {
// 讀操作
}
@DS("master")
public void writeOperation() {
// 寫操作
}
}
9.2 動態(tài)增減數據源
@Service
public class DynamicDataSourceService {
@Autowired
private DynamicDataSourceProvider dataSourceProvider;
/**
* 運行時動態(tài)添加數據源
*/
public void addDataSource(String dataSourceName, DataSourceProperty property) {
DynamicDataSourceContextHolder.addDataSource(dataSourceName, property);
log.info("動態(tài)添加數據源: {}", dataSourceName);
}
/**
* 運行時移除數據源
*/
public void removeDataSource(String dataSourceName) {
DynamicDataSourceContextHolder.removeDataSource(dataSourceName);
log.info("動態(tài)移除數據源: {}", dataSourceName);
}
}
總結
通過SpringBoot + MyBatis-Plus + Dynamic-Datasource實現讀寫分離,可以顯著提升數據庫讀性能和高可用性。關鍵點包括:
- 正確配置:確保主從數據源配置正確,負載均衡策略合理
- 合理使用注解:在適當的方法上使用
@DS注解或通過AOP自動切換 - 事務管理:注意事務內的數據源行為,關鍵操作強制走主庫
- 監(jiān)控告警:建立完善的監(jiān)控體系,及時發(fā)現主從延遲等問題
- 故障處理:實現從庫故障自動轉移和恢復機制
這種架構在讀寫比例高的應用中能帶來顯著的性能提升,但也需要處理好主從同步延遲等一致性問題。
到此這篇關于SpringBoot+MyBatis-Plus+Dynamic-Datasource讀寫分離完整指南的文章就介紹到這了,更多相關SpringBoot MyBatis-Plus 讀寫分離內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
struts1登錄示例代碼_動力節(jié)點Java學院整理
這篇文章主要介紹了struts1登錄示例代碼,需要的朋友可以參考下2017-08-08
解決springboot項目啟動報錯Field xxxMapper in com...xx
這篇文章主要介紹了解決springboot項目啟動報錯Field xxxMapper in com...xxxContr問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12

