MyBatis-Plus 多数据源配置实战:从读写分离到分库分表
作为在分布式系统摸爬滚打七年的老兵,我深知多数据源场景是后端开发的必经之路。无论是读写分离优化性能,还是分库分表应对海量数据,亦或是整合第三方异构数据源,MyBatis-Plus 的多数据源配置都是核心解决方案。本文将通过银行核心系统的真实业务场景,带你从 0 到 1 掌握多数据源配置全流程,包含实战代码、坑点解析和最佳实践。
一、为什么需要多数据源?先搞懂业务场景
典型应用场景(附银行案例)
- 读写分离(90% 的场景)
-
- 主库(master)写:账户开户、转账等写操作
-
- 从库(slave)读:账户余额查询、交易流水统计场景:每日千万级查询流量,通过读写分离减轻主库压力
- 分库分表(数据量超 5000 万)
-
- 按用户 ID 分库:user_0、user_1 库(尾号 0-4/5-9)
-
- 按时间分表:trade_2023、trade_2024 表(按年份拆分)场景:银行交易表日均百万条,分库分表提升查询效率
- 多业务系统集成
-
- 核心业务库(账户、交易)
-
- 报表统计库(OLAP 分析)
-
- 第三方对接库(支付渠道、征信系统)场景:统一服务同时访问多个独立数据库
技术方案对比(新手必看)
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
原生 JDBC 切换 | 可控性强 | 代码侵入性高,重复劳动多 | 简单双数据源 |
AbstractRoutingDataSource | Spring 原生支持 | 配置繁琐,功能有限 | 基础读写分离 |
DynamicDataSource | 零代码入侵,支持动态切换 | 依赖第三方库(需选靠谱的) | 复杂多数据源场景 |
推荐方案:使用 Dynamic Data Source(MyBatis-Plus 官方推荐),10 行配置实现动态数据源切换,兼容所有 Spring Boot 版本。
二、实战准备:搭建多数据源项目骨架
1. 添加核心依赖(避开版本陷阱)
<dependencies>
<!-- 原有依赖:Spring Web、MyBatis-Plus、MySQL驱动、Lombok -->
<!-- 新增多数据源依赖(注意版本匹配) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version> <!-- 2025年稳定版,与MyBatis-Plus 4.5+兼容 -->
</dependency>
<!-- 若需支持XA分布式事务,添加以下依赖(本案例暂不涉及) -->
<!-- <dependency>groupId>com.atomikos</groupId><artifactId>transactions-jdbc</artifactId></dependency> -->
</dependencies>
2. 目录结构规划(清晰的工程规范)
src/main/java/com/example
├── annotation # 自定义数据源注解
│ └── DS.java
├── config # 多数据源配置类
│ ├── DataSourceConfig.java
│ └── MyBatisPlusConfig.java
├── entity # 实体类(不同数据源可分模块,如core/stat)
├── mapper # Mapper接口(分库时按库名分组,如master/slave)
├── service # 业务层(包含数据源切换逻辑)
└── controller # 控制层
三、核心配置:3 步搞定读写分离(以主从库为例)
1. application.yml 配置(关键细节解析)
spring:
datasource:
dynamic:
primary: master # 默认数据源(写操作)
strict: false # 严格模式,未定义数据源时抛异常(生产环境建议true)
datasource:
master: # 主库(写)
url: jdbc:mysql://localhost:3306/core_db?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource # 连接池(性能优于Tomcat默认)
hikari:
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
slave: # 从库(读)
url: jdbc:mysql://localhost:3306/core_db_slave?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml # 通用Mapper路径
global-config:
db-config:
table-prefix: tb_ # 表名前缀(所有数据源统一)
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(区分数据源)
2. 自定义数据源注解(优雅的切换方式)
package com.example.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE}) // 可用于方法或类
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String value() default ""; // 数据源名称(如"master"、"slave")
}
3. Service 层实现数据源切换(零侵入代码)
package com.example.service;
import com.example.annotation.DS;
import com.example.entity.Account;
import com.example.mapper.AccountMapper;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
private final AccountMapper accountMapper;
public AccountService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
// 写操作:使用默认主库(无需注解,或显式@DS("master"))
public boolean createAccount(Account account) {
return accountMapper.insert(account) > 0;
}
// 读操作:指定从库(关键业务加注解)
@DS("slave")
public Account getAccountById(Long id) {
return accountMapper.selectById(id);
}
// 复杂场景:方法内动态切换(非注解方式)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 1. 查询转出账户(从库)
DynamicDataSourceContextHolder.push("slave");
Account fromAccount = accountMapper.selectById(fromId);
// 2. 更新账户余额(主库,自动恢复默认数据源)
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
accountMapper.updateById(fromAccount); // 使用主库
}
}
四、分库分表进阶:按用户 ID 动态路由(银行核心场景)
业务需求:用户 ID 尾号 0-4 存 user_0 库,5-9 存 user_1 库
1. 扩展数据源配置(新增分库)
spring:
datasource:
dynamic:
datasource:
user_0:
url: jdbc:mysql://localhost:3306/user_0_db?serverTimezone=Asia/Shanghai
# 其他配置同master
user_1:
url: jdbc:mysql://localhost:3306/user_1_db?serverTimezone=Asia/Shanghai
# 其他配置同master
2. 实现自定义路由策略(关键代码)
package com.example.config;
import com.baomidou.dynamic.datasource.router.AbstractDataSourceRouter;
import com.baomidou.dynamic.datasource.toolkit.StringUtils;
import org.springframework.stereotype.Component;
@Component
public class UserDataSourceRouter extends AbstractDataSourceRouter {
@Override
public String determineDatasource() {
// 从线程变量获取用户ID(实际场景从请求或上下文获取)
Long userId = UserContextHolder.getUserId();
if (userId == null) {
return getPrimaryDatasource(); // 使用默认主库
}
int mod = userId.intValue() % 10;
return mod < 5 ? "user_0" : "user_1"; // 根据尾号选择分库
}
}
3. 线程上下文工具类(存储路由参数)
package com.example.util;
public class UserContextHolder {
private static final ThreadLocal<Long> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setUserId(Long userId) {
CONTEXT_HOLDER.set(userId);
}
public static Long getUserId() {
return CONTEXT_HOLDER.get();
}
public static void clear() {
CONTEXT_HOLDER.remove();
}
}
五、事务处理:多数据源下的事务一致性(核心难点)
1. 单数据源事务(简单场景)
@Service
public class AccountService {
@Transactional(rollbackFor = Exception.class) // 自动使用当前数据源
public void updateBalance(Long id, BigDecimal amount) {
Account account = getAccountById(id);
account.setBalance(amount);
updateById(account);
}
}
2. 跨数据源事务(分布式事务)
方案一:XA 强一致性(适合金融场景)
<!-- 添加Atomikos依赖 -->
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>5.2.1</version>
</dependency>
@Service
public class TransferService {
@Resource
private PlatformTransactionManager transactionManager; // Atomikos事务管理器
public void crossDbTransfer(Long fromId, Long toId, BigDecimal amount) {
UserTransaction userTransaction = AtomikosContext.getThreadLocalUserTransaction();
try {
userTransaction.begin();
// 转出(user_0库)
UserContextHolder.setUserId(fromId);
accountService.updateBalance(fromId, fromAccount.getBalance().subtract(amount));
// 转入(user_1库)
UserContextHolder.setUserId(toId);
accountService.updateBalance(toId, toAccount.getBalance().add(amount));
userTransaction.commit();
} catch (Exception e) {
userTransaction.rollback();
throw new RuntimeException("跨库转账失败");
}
}
}
方案二:最终一致性(适合非核心场景)
通过消息队列(如 Kafka)异步补偿,降低事务复杂度,牺牲强一致性换取性能。
六、实战验证:3 个必测场景确保配置正确
1. 读写分离验证(日志查看)
- 执行写操作(无 @DS 注解),日志输出:Using data source 'master'
- 执行读操作(@DS ("slave")),日志输出:Using data source 'slave'
2. 分库路由验证(数据库查看)
- 插入用户 ID=1(尾号 1,user_0 库):SELECT * FROM user_0_db.tb_account WHERE id=1
- 插入用户 ID=6(尾号 6,user_1 库):SELECT * FROM user_1_db.tb_account WHERE id=6
3. 事务回滚验证(模拟异常)
@DS("master")
@Transactional
public void testTransaction() {
Account account = new Account();
account.setAccountNo("A004");
save(account); // 插入成功
int i = 1 / 0; // 主动抛异常
account.setId(1L);
updateById(account); // 不会执行,数据回滚
}
七、老司机排坑指南:这些陷阱必须避开
1. 数据源优先级问题
- 注解 > 路由策略 > 默认数据源:方法上的 @DS 优先级最高,其次是自定义路由,最后用 primary 配置
2. MyBatis-Plus 扫描路径冲突
分库场景需按数据源分组扫描 Mapper:
@Configuration
public class MyBatisPlusConfig {
@Bean
@DS("user_0") // 指定数据源
public SqlSessionFactory user0SqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/user0/**/*.xml")); // 独立Mapper路径
return factory.getObject();
}
}
3. 连接池配置不当
- 从库连接池可配置更大的 maximum-pool-size(读多场景)
- 主库配置更小的 connection-timeout(减少写阻塞)
4. 分页插件冲突
多数据源下需为每个数据源单独配置分页插件:
@Bean
@ConfigurationProperties(prefix = "mybatis-plus")
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
八、最佳实践:生产环境必备配置
1. 数据源监控(Prometheus+Grafana)
spring:
datasource:
dynamic:
datasource:
master:
hikari:
metrics-track-creation: true # 开启连接池监控指标
2. 数据源切换日志
// 在自定义路由策略中添加日志
public class UserDataSourceRouter {
private static final Logger log = LoggerFactory.getLogger(UserDataSourceRouter.class);
@Override
public String determineDatasource() {
String ds = super.determineDatasource();
log.info("Using datasource: {}", ds);
return ds;
}
}
3. 动态数据源热加载(高级特性)
// 运行时新增数据源(如对接第三方临时库)
DynamicDataSourceContextHolder.registry("temp_db", dataSourceProperties);
九、总结:多数据源配置的核心思维
- 分层设计
-
- 基础层:通过 Dynamic Data Source 实现数据源快速切换
-
- 策略层:自定义路由算法(尾号分库、租户隔离等)
-
- 事务层:根据业务特性选择强一致 / 最终一致方案
- 最小侵入原则优先使用注解(@DS)和线程上下文(ThreadLocal)切换,避免在 Mapper 层硬编码数据源
- 可观测性每个数据源配置独立的日志、监控指标,出现问题时快速定位是主库写入失败还是从库同步延迟