MyBatis-Plus 多数据源动态切换接入记录

MyBatis-Plus 多数据源动态切换接入记录

项目里有三个库:核心业务库(主从各一个)+ 日志库(单独存操作日志)。早期的做法是手动配了三个 DataSource Bean,加上三套 SqlSessionFactory,光配置类就写了两三百行,还经常忘了在哪个 Mapper 上指定 sqlSessionTemplate

接入 dynamic-datasource-spring-boot-starter 之后,配置文件加几行,业务层加个 @DS 注解,问题解决。


一、使用场景

什么情况下需要多数据源:

  • 读写分离:主库负责写,从库负责读,减轻主库压力
  • 业务隔离:核心业务库 + 日志库 + 报表库,不同业务操作不同数据库
  • 数据集成:需要从外部系统的数据库读取数据(如 ERP、财务系统)

为什么不手动配多个 DataSource Bean:

  • 手动配置需要维护多套 SqlSessionFactory + TransactionManager,代码量大
  • 切换数据源时要在 ThreadLocal 里手动设置,业务代码和数据源逻辑耦合
  • dynamic-datasource 通过 @DS 注解声明式切换,AOP 拦截,业务代码几乎无感知

二、接入步骤

引入依赖

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>

配置数据源

spring:
datasource:
dynamic:
primary: master # 默认数据源,不加 @DS 注解时使用这个
strict: false # false:找不到指定数据源时回落到 primary;true:直接抛异常
datasource:
master:
url: jdbc:mysql://localhost:3306/main_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ENC(密文)
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
slave:
url: jdbc:mysql://localhost:3307/main_db?useSSL=false&serverTimezone=Asia/Shanghai
username: readonly
password: ENC(密文)
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
log:
url: jdbc:mysql://localhost:3306/log_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ENC(密文)
driver-class-name: com.mysql.cj.jdbc.Driver

@DS 注解使用

// 作用在类上:该类所有方法默认使用 slave 数据源
@DS("slave")
@Service
public class ProductQueryService {
// 所有查询走从库
public Product getById(Long id) { ... }
}

// 作用在方法上:只有这个方法使用 log 数据源(优先级高于类级别)
@DS("log")
public void saveOperationLog(OperationLog log) {
operationLogMapper.insert(log);
}

// 不加注解:使用 primary 配置的默认数据源(master)
public void createOrder(Order order) {
orderMapper.insert(order);
}

优先级:方法级 @DS > 类级 @DS > primary 默认值


三、事务控制注意事项

这是使用多数据源最容易踩的坑,要单独说清楚。

问题:跨数据源的操作,Spring 的 @Transactional 无法保证原子性。

根因@Transactional 绑定的是单个 DataSource 的连接,切换数据源后是新的独立连接,两者不在同一个事务里。

// ❌ 错误用法:期望 master 和 log 两个库的操作在同一个事务里
@Transactional
public void createOrderWithLog(Order order) {
orderMapper.insert(order); // master 库
operationLogService.save(...); // @DS("log") 的方法 → log 库,不在 master 的事务里
// 如果 operationLogService.save 抛异常,order 已经提交,无法回滚
}

正确处理方式

// ✅ 正确:各数据源的事务分开管理
public void createOrderWithLog(Order order) {
createOrder(order); // master 库,有自己的事务
saveLog(order); // log 库,有自己的事务,两者相互独立
}

@Transactional // 只管 master 库的事务
public void createOrder(Order order) {
orderMapper.insert(order);
}

@DS("log")
@Transactional // 只管 log 库的事务
public void saveLog(Order order) {
operationLogMapper.insert(buildLog(order));
}

如果确实需要跨库原子性,需要引入分布式事务(Seata)。但实际业务中,日志库的写入失败不应该影响业务主流程,大多数情况下把日志写入做成异步(MQ 或 @Async)更合理。


四、结合 MyBatis-Plus 的注意事项

@DS 加在 Service 层还是 Mapper 层

两层都可以,但推荐加在 Service 层:

  • Mapper 层的职责是 SQL 操作,数据源选择是业务逻辑,放 Service 层语义更清晰
  • 同一个 Mapper 在不同业务场景下可能走不同数据源,Service 层更灵活
// 推荐:Service 层加 @DS,Mapper 不感知数据源
@DS("slave")
public List<Product> listForDisplay() {
return productMapper.selectList(...);
}

@DS("master")
public void updateStock(Long productId, int delta) {
productMapper.updateStock(productId, delta);
}

与 ShardingSphere 同时使用

dynamic-datasource 支持将 ShardingSphere 的 DataSource 作为其中一个节点挂载进来,两者可以共存:

spring:
datasource:
dynamic:
primary: master
datasource:
master:
# 普通数据源
url: jdbc:mysql://localhost:3306/main_db
sharding:
# 将 ShardingSphere 数据源作为一个节点
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yaml

五、适用边界

需求推荐方案
读写分离(中小型项目)dynamic-datasource
多业务库隔离dynamic-datasource
需要跨库原子事务dynamic-datasource + Seata
数据量大需要分片ShardingSphere
分片 + 读写分离ShardingSphere(内置读写分离支持)

极高 QPS 场景下,dynamic-datasource 的 AOP 拦截有微小性能损耗(通常在微秒级,可忽略不计),正常业务场景不需要担心这个。