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 strict: false 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 注解使用
@DS("slave") @Service public class ProductQueryService { public Product getById(Long id) { ... } }
@DS("log") public void saveOperationLog(OperationLog log) { operationLogMapper.insert(log); }
public void createOrder(Order order) { orderMapper.insert(order); }
|
优先级:方法级 @DS > 类级 @DS > primary 默认值
三、事务控制注意事项
这是使用多数据源最容易踩的坑,要单独说清楚。
问题:跨数据源的操作,Spring 的 @Transactional 无法保证原子性。
根因:@Transactional 绑定的是单个 DataSource 的连接,切换数据源后是新的独立连接,两者不在同一个事务里。
@Transactional public void createOrderWithLog(Order order) { orderMapper.insert(order); operationLogService.save(...); }
|
正确处理方式:
public void createOrderWithLog(Order order) { createOrder(order); saveLog(order); }
@Transactional public void createOrder(Order order) { orderMapper.insert(order); }
@DS("log") @Transactional public void saveLog(Order order) { operationLogMapper.insert(buildLog(order)); }
|
如果确实需要跨库原子性,需要引入分布式事务(Seata)。但实际业务中,日志库的写入失败不应该影响业务主流程,大多数情况下把日志写入做成异步(MQ 或 @Async)更合理。
四、结合 MyBatis-Plus 的注意事项
@DS 加在 Service 层还是 Mapper 层
两层都可以,但推荐加在 Service 层:
- Mapper 层的职责是 SQL 操作,数据源选择是业务逻辑,放 Service 层语义更清晰
- 同一个 Mapper 在不同业务场景下可能走不同数据源,Service 层更灵活
@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: 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 拦截有微小性能损耗(通常在微秒级,可忽略不计),正常业务场景不需要担心这个。