Spring Boot + MyBatis-Plus 优雅实现 MySQL & Doris 双数据源动态切换实战
前言
随着业务的不断发展,系统逐渐面临着 OLTP(联机事务处理) 与 OLAP(联机分析处理) 混合使用的场景。为了提升查询性能并减轻主库压力,我们引入了 Apache Doris 作为分析型数据仓库。
这就带来了一个实际问题:如何在同一个 Spring Boot 项目中,优雅地实现 MySQL(负责业务增删改)与 Doris(负责海量数据复杂查询)的双数据源动态切换?
本文将结合近期的实际项目改造经验,从方案选型、配置实战、代码改造到踩坑记录,完整复盘基于 MyBatis-Plus + dynamic-datasource 的双数据源落地指南。
一、 方案选型:原生分包 vs 动态数据源
在改造初期,我们调研了两种主流的双数据源实现方案:
| 对比项 | 方案A:原生按包路径分包 | 方案B:dynamic-datasource 注解路由 |
|---|---|---|
| 配置复杂度 | 高(需手写 DataSourceConfig、SqlSessionFactory 等 Java Config) |
低(纯 YAML 配置,零 Java Config) |
| 数据源切换 | 按 Mapper 包路径自动路由 | @DS 注解显式声明 |
| 对老业务影响 | 大(需重构包结构,调整 @MapperScan) |
零侵入(只需在原有 Mapper 上加注解) |
| 灵活性 | 低(固定包路径约定) | 高(可精确控制到类或方法级别) |
💡 最终决策:
考虑到对现有老业务的侵入性以及后期的维护成本,我们果断放弃了原生分包方案,采用了苞米豆(MyBatis-Plus 作者)开源的 dynamic-datasource-spring-boot-starter 结合 @DS 注解的方案。
数据源职责划分:
- MySQL (Primary):作为默认数据源,负责未来新业务的 OLTP 关系型数据存储。未标注
@DS的方法自动走此库。 - Doris (Slave):作为分析数据源,负责现有老业务的 OLAP 复杂查询。通过
@DS("doris")显式指定。
二、 核心改造步骤
1. 引入 Maven 依赖
在业务模块的 pom.xml 中,在原有的 mybatis-plus-boot-starter 之后,引入动态数据源 Starter:
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- ========== 动态数据源核心 Starter(本次改造新增) ========== -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
⚠️ 注意:不需要额外引入
druid依赖,dynamic-datasource-spring-boot-starter内部已经集成了 Druid 连接池。
2. YAML 多数据源配置
将原有的单数据源配置改造为 spring.datasource.dynamic 结构。以下是脱敏后的核心配置示例:
spring:
datasource:
dynamic:
# 【核心】指定默认(主)数据源名称,所有未标注 @DS 的方法都走该数据源
primary: mysql
# 【核心】strict: false 表示当找不到指定数据源时,回退到 primary 数据源,不抛异常
strict: false
# 【核心】所有数据源的声明区域
datasource:
# ========== 数据源1:MySQL 主库(默认 OLTP)==========
mysql:
url: jdbc:mysql://mysql-host:3306/oltp_db?autoReconnect=true&socketTimeout=600000&rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: mysql_user
password: your_password
druid:
initial-size: 5
min-idle: 5
max-active: 200
max-wait: 10000
time-between-eviction-runs-millis: 600000
min-evictable-idle-time-millis: 300000
test-while-idle: true
validation-query: SELECT 1
# ========== 数据源2:Doris 分析库(OLAP)==========
doris:
# Doris 兼容 MySQL 协议,直接使用 mysql 驱动即可
url: jdbc:mysql://doris-host:9030/olap_db?autoReconnect=true&socketTimeout=600000&useUnicode=true&characterEncoding=utf-8
username: doris_user
password: your_password
druid:
initial-size: 5
min-idle: 5
max-active: 200
max-wait: 10000
time-between-eviction-runs-millis: 600000
min-evictable-idle-time-millis: 300000
test-while-idle: true
validation-query: SELECT 1
3. 代码改造与 @DS 注解使用
改造原则:
- 老业务(OLAP):为所有现有的分析类 Mapper 接口添加
@DS("doris")。 - 新业务(OLTP):不添加
@DS注解,自动走primary指定的 MySQL。
改造示例:
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.dynamic.datasource.annotation.DS; // ← 新增 import
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.OrderAnalysis;
@Mapper
@DS("doris") // ← 新增注解,指定该 Mapper 走 Doris 数据源
public interface OrderAnalysisMapper extends BaseMapper<OrderAnalysis> {
// 复杂的 OLAP 统计查询...
}
📌 @DS 注解优先级(重要):
@DS 注解可以标注在多个层级,优先级由高到低为:
- Mapper 接口方法上(最高)
- Service 实现类的方法上
- Service 实现类上
- Mapper 接口上(最低)
最佳实践建议:现有业务保持 Mapper 接口类级别 的 @DS("doris") 作为安全兜底;未来如有个别方法需要切换数据源,可在 方法级别 覆盖类级别配置。尽量避免在 Service 和 Mapper 上同时标注 @DS 以免造成逻辑混淆。
三、 避坑指南:Druid 连接池参数校验失败
在改造过程中,我们遇到了一个非常隐蔽的启动报错:
java.sql.SQLException: keepAliveBetweenTimeMillis must be greater than timeBetweenEvictionRunsMillis
🔍 问题排查:
- 我们参考了公司内部另一个成熟项目的配置,其中包含了
keep-alive-between-time-millis参数。 - 老项目使用的是 Druid
1.2.18,不支持该参数,配置被静默忽略,所以没报错。 - 本项目通过
dynamic-datasource 3.4.1传递依赖引入了 Druid1.2.24,该版本支持并严格校验此参数。 - 但
dynamic-datasource 3.4.1存在一个小 Bug,未将keep-alive-between-time-millis正确传递到底层DruidDataSource,导致 Druid 使用了默认值120000(2分钟)。 - 而我们配置的
time-between-eviction-runs-millis为600000(10分钟)。 - 校验逻辑:
120000 (默认) <= 600000 (配置)❌ 不满足大于的条件,直接抛出异常!
✅ 解决方案:
果断从 YAML 配置中移除以下两个参数,保留其他常规 Druid 参数即可正常启动:
keep-alive-between-time-millis: 1200000keep-alive: true
四、 新业务开发规范与事务管理
1. 跨数据源查询注意事项
- 严禁跨库事务:同一 Service 方法内不要混合操作两个数据源,否则 Spring 的
@Transactional无法统一管理跨库事务。理论上不能设计这种业务,也就不存在跨库查询。 - 数据聚合:如需聚合两个库的数据,请在 Service 层分别调用各自的 Mapper 获取数据后,在内存中进行组装。
- 业务隔离:在我们的架构中,MySQL 负责 OLTP 写入,Doris 负责 OLAP 查询,两库在业务层面天然隔离,不存在跨库事务需求。
2. 事务管理
直接使用 Spring 标准的 @Transactional 即可,dynamic-datasource 会自动为对应的数据源创建 DataSourceTransactionManager:
@Service
public class NewBusinessServiceImpl implements NewBusinessService {
@Autowired
private NewBusinessMapper newBusinessMapper; // 未加@DS,默认走 MySQL
// 自动在 mysql 数据源上开启事务
@Transactional(rollbackFor = Exception.class)
public void saveBatch(List<NewBusinessEntity> list) {
for (NewBusinessEntity entity : list) {
newBusinessMapper.insert(entity);
}
}
}
五、 本地开发调试技巧 (Nacos 环境)
如果你的项目使用了 Nacos 作为配置中心,本地调试双数据源时可能会遇到远程配置覆盖本地配置的问题。
本地调试正确姿势:
- 禁用 Nacos Config:防止远程配置覆盖本地的
application-dev.yml。 - 激活本地 Profile。
IDEA 启动配置:
- Active profiles:
dev - VM options:
-Dspring.cloud.nacos.config.enabled=false
命令行启动:
java -Dspring.profiles.active=dev -Dspring.cloud.nacos.config.enabled=false -jar app.jar
⚠️ 关键提醒:
-D参数必须放在-jar之前,否则会被 JVM 当成程序参数而非系统属性,导致禁用失效!
六、 总结与验证清单
改造完成后,可以通过以下清单进行验收:
| 验证项 | 验证方法 | 期望结果 |
|---|---|---|
| 编译检查 | mvn clean compile |
BUILD SUCCESS |
| 数据源初始化 | 查看启动日志 | 看到 mysql 和 doris 两个数据源的 inited 成功日志 |
| Doris 路由 | 调用老业务分析接口 | SQL 日志显示连接的是 Doris 的 Host 和 Port |
| MySQL 路由 | 调用新业务写入接口 | SQL 日志显示连接的是 MySQL 的 Host 和 Port |
| 事务验证 | 故意在 MySQL 事务中抛出异常 | 数据成功回滚,事务管理器工作正常 |
结语:
通过引入 dynamic-datasource,我们以极低的代码侵入性完成了 OLTP 与 OLAP 数据源的物理隔离。不仅提升了系统的查询性能,也为后续新业务的迭代提供了标准化的开发规范。希望本文的实战经验和踩坑记录,能为正在进行多数据源改造的你提供一些参考!
如果这篇文章对你有帮助,欢迎点赞、收藏、评论交流!你的支持是我持续分享技术干货的最大动力!