Spring Boot + MyBatis-Plus 优雅实现 MySQL & Doris 双数据源动态切换实战

Source

Spring Boot + MyBatis-Plus 优雅实现 MySQL & Doris 双数据源动态切换实战

前言

随着业务的不断发展,系统逐渐面临着 OLTP(联机事务处理)OLAP(联机分析处理) 混合使用的场景。为了提升查询性能并减轻主库压力,我们引入了 Apache Doris 作为分析型数据仓库。

这就带来了一个实际问题:如何在同一个 Spring Boot 项目中,优雅地实现 MySQL(负责业务增删改)与 Doris(负责海量数据复杂查询)的双数据源动态切换?

本文将结合近期的实际项目改造经验,从方案选型、配置实战、代码改造到踩坑记录,完整复盘基于 MyBatis-Plus + dynamic-datasource 的双数据源落地指南。


一、 方案选型:原生分包 vs 动态数据源

在改造初期,我们调研了两种主流的双数据源实现方案:

对比项 方案A:原生按包路径分包 方案B:dynamic-datasource 注解路由
配置复杂度 高(需手写 DataSourceConfigSqlSessionFactory 等 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 注解可以标注在多个层级,优先级由高到低为:

  1. Mapper 接口方法上(最高)
  2. Service 实现类的方法上
  3. Service 实现类上
  4. Mapper 接口上(最低)

最佳实践建议:现有业务保持 Mapper 接口类级别@DS("doris") 作为安全兜底;未来如有个别方法需要切换数据源,可在 方法级别 覆盖类级别配置。尽量避免在 Service 和 Mapper 上同时标注 @DS 以免造成逻辑混淆。


三、 避坑指南:Druid 连接池参数校验失败

在改造过程中,我们遇到了一个非常隐蔽的启动报错:

java.sql.SQLException: keepAliveBetweenTimeMillis must be greater than timeBetweenEvictionRunsMillis

🔍 问题排查:

  1. 我们参考了公司内部另一个成熟项目的配置,其中包含了 keep-alive-between-time-millis 参数。
  2. 老项目使用的是 Druid 1.2.18,不支持该参数,配置被静默忽略,所以没报错。
  3. 本项目通过 dynamic-datasource 3.4.1 传递依赖引入了 Druid 1.2.24,该版本支持并严格校验此参数。
  4. dynamic-datasource 3.4.1 存在一个小 Bug,未将 keep-alive-between-time-millis 正确传递到底层 DruidDataSource,导致 Druid 使用了默认值 120000(2分钟)。
  5. 而我们配置的 time-between-eviction-runs-millis600000(10分钟)。
  6. 校验逻辑120000 (默认) <= 600000 (配置) ❌ 不满足大于的条件,直接抛出异常!

✅ 解决方案:
果断从 YAML 配置中移除以下两个参数,保留其他常规 Druid 参数即可正常启动:

  • keep-alive-between-time-millis: 1200000
  • keep-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 作为配置中心,本地调试双数据源时可能会遇到远程配置覆盖本地配置的问题。

本地调试正确姿势:

  1. 禁用 Nacos Config:防止远程配置覆盖本地的 application-dev.yml
  2. 激活本地 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
数据源初始化 查看启动日志 看到 mysqldoris 两个数据源的 inited 成功日志
Doris 路由 调用老业务分析接口 SQL 日志显示连接的是 Doris 的 Host 和 Port
MySQL 路由 调用新业务写入接口 SQL 日志显示连接的是 MySQL 的 Host 和 Port
事务验证 故意在 MySQL 事务中抛出异常 数据成功回滚,事务管理器工作正常

结语
通过引入 dynamic-datasource,我们以极低的代码侵入性完成了 OLTP 与 OLAP 数据源的物理隔离。不仅提升了系统的查询性能,也为后续新业务的迭代提供了标准化的开发规范。希望本文的实战经验和踩坑记录,能为正在进行多数据源改造的你提供一些参考!


如果这篇文章对你有帮助,欢迎点赞、收藏、评论交流!你的支持是我持续分享技术干货的最大动力!