MyBatis-Plus多数据源实战:被DBA追着改配置后,我肝出这份避坑指南(附动态切换源码)

作者:不想打工的码农

原创声明:本文基于政务系统数据迁移真实项目,所有配置、监控截图、故障复盘均脱敏处理,拒绝"复制粘贴式教学"


一、血泪开场:那个被DBA堵在工位的下午

"新库连不上!旧库写穿了!监控报警刷屏!"

DBA大哥把咖啡杯蹾在我桌上:"兄弟,再出问题,今晚咱俩一起通宵修数据。"

我盯着控制台疯狂滚动的CannotGetJdbcConnectionException,冷汗浸透衬衫------
就因为多数据源配置少写了个@DS?!

这不是演习。去年负责省级政务系统迁移时,因多数据源配置疏漏,导致旧库被误写入测试数据 ,紧急回滚3小时。从此我立下flag:多数据源,必须吃透!


二、为什么选dynamic-datasource?血泪对比实录

方案 ShardingSphere 手动配置AbstractRoutingDataSource dynamic-datasource(推荐)
学习成本 高(需理解分片规则) 极高(重写数据源路由逻辑) 低(注解即用)
动态扩展 需重启 需重启 运行时动态增删
事务支持 复杂 易出错 @DS与@Transactional完美兼容
踩坑指数 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
我的选择 ❌ 迁移场景不需要分片 ❌ 通宵写路由逻辑后崩溃 30分钟搞定核心配置

💡 关键洞察

多数据源≠分库分表!单纯读写分离/新旧库迁移场景,dynamic-datasource是性价比之王(GitHub 18k+ star,国产之光!)


三、手把手配置:从"连不上"到"稳如老狗"

🔑 第一步:Maven依赖(避坑重点!)

xml 复制代码
<!-- 核心:必须指定版本!避免与Spring Boot版本冲突 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.2</version> <!-- 亲测3.5.1有事务bug! -->
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!-- 连接池:生产环境必加监控 -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>

⚠️ 血泪教训

  • 曾因未指定版本,引入3.4.0导致@DS在事务中失效,线上数据错乱!
  • 务必检查依赖树mvn dependency:tree | grep datasource

🔑 第二步:YML配置(生产级模板)

yaml 复制代码
spring:
  datasource:
    dynamic:
      # 默认数据源(必填!否则启动报错)
      primary: old_db 
      # 严格模式:未找到数据源时抛异常(开发环境关闭,生产开启!)
      strict: true 
      datasource:
        # 旧库(主库,承担写操作)
        old_db:
          url: jdbc:mysql://old-prod:3306/gov_db?useSSL=false&serverTimezone=Asia/Shanghai
          username: prod_writer
          password: ${OLD_DB_PWD} # 密码从配置中心拉取!
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            connection-timeout: 30000
            maximum-pool-size: 20
            # 【关键】监控连接泄漏(超过30秒未归还报警)
            leak-detection-threshold: 30000 
        # 新库(从库,迁移期间只读)
        new_db:
          url: jdbc:mysql://new-prod:3306/gov_db_v2?useSSL=false
          username: prod_reader
          password: ${NEW_DB_PWD}
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            read-only: true # 强制只读!防手抖写入
            maximum-pool-size: 30
      # 【神配置】慢SQL监控(超过2秒打印日志)
      p6spy: true 

生产加固点

  1. 密码绝不硬编码!对接Apollo/Nacos配置中心
  2. read-only: true 为从库上"保险栓"
  3. leak-detection-threshold 捕捉连接泄漏(曾靠它发现未关闭的ResultSet)

🔑 第三步:代码级动态切换(核心!)

typescript 复制代码
// 1. Service层:注解指定数据源
@Service
public class DataMigrateService {
    
    @Autowired
    private OldUserMapper oldUserMapper; // 旧库Mapper
    
    @Autowired
    private NewUserMapper newUserMapper; // 新库Mapper
    
    // 【关键】@DS指定数据源,支持嵌套调用
    @DS("old_db")
    public List<User> queryFromOld() {
        return oldUserMapper.selectList(null); // 从旧库查
    }
    
    @DS("new_db")
    @Transactional // 事务内切换?看下文避坑指南!
    public boolean saveToNew(User user) {
        return newUserMapper.insert(user) > 0; // 写入新库
    }
    
    // 2. 复杂场景:方法内动态切换(AOP失效时救命用)
    public void complexMigrate() {
        // 临时切换到旧库
        DynamicDataSourceContextHolder.push("old_db");
        try {
            List<User> users = oldUserMapper.selectList(null);
            // 切回新库写入
            DynamicDataSourceContextHolder.push("new_db");
            users.forEach(newUserMapper::insert);
        } finally {
            // 【必须】清理上下文!否则线程复用导致数据源错乱
            DynamicDataSourceContextHolder.poll();
            DynamicDataSourceContextHolder.poll();
        }
    }
}

💡 灵魂注释

  • @DS 放在Service层!Mapper层加无效(亲测踩坑)
  • DynamicDataSourceContextHolder.poll() 必须成对出现,否则线程池污染(曾导致用户A查到用户B数据!)

四、生产避坑指南(DBA认证版)

坑点 现象 解决方案
事务内切换失效 @Transactional + @DS 嵌套时,始终走默认库 1. 事务方法内避免切换 2. 用@Transactional(propagation = Propagation.REQUIRES_NEW)新开事务
连接泄漏 监控显示活跃连接持续上涨 1. 开启leak-detection-threshold 2. 检查MyBatis resultMap是否关闭ResultSet
Druid监控空白 访问/druid看不到SQL 添加配置:spring.datasource.dynamic.druid.web-stat-filter.enabled=true
多模块冲突 其他模块引入ShardingSphere导致Bean冲突 排除依赖:<exclusions><exclusion>...sharding...</exclusion></exclusions>

🌰 事务切换真实案例

less 复制代码
// 错误写法:事务内切换,new_db操作实际走old_db!
@Transactional
public void migrateWithError(User user) {
    oldUserMapper.delete(user.getId()); // old_db
    @DS("new_db") // 无效!事务已绑定old_db
    newUserMapper.insert(user); 
}

// 正确写法:拆分为两个事务方法
@Transactional
@DS("old_db")
public void deleteFromOld(Long id) { ... }

@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS("new_db")
public void insertToNew(User user) { ... }

// 调用处
public void safeMigrate(User user) {
    deleteFromOld(user.getId());
    insertToNew(user); // 新事务,数据源生效
}

五、迁移实战:零停机切换的骚操作

背景:政务系统需将2000万用户数据从旧库迁至新库,要求业务不停机

📊 我的四步迁移法

  1. 双写阶段(1周)

    • 所有写操作同时写old_db + new_db
    • @DS + AOP自动双写(代码略,私信可发)
    • 监控重点:new_db写入延迟、数据一致性校验
  2. 校验阶段(3天)

    • 每日跑对比脚本:SELECT COUNT(*) FROM user WHERE update_time > '昨日'
    • 差异数据自动修复(附校验脚本核心逻辑)
  3. 切读阶段(凌晨2点)

    • 修改primary: new_db,重启服务
    • 灰度策略:先切10%流量,观察1小时监控
  4. 下线旧库(1周后)

    • 确认无异常后,注释old_db配置
    • 保留旧库30天(防回滚)

成果

  • 迁移期间0故障,用户无感知
  • DBA主动加我微信:"下次迁移还找你!"

六、监控大屏:让风险看得见

(文字描述监控效果)

登录Grafana大屏:

  • 连接池水位:old_db活跃连接稳定在5,new_db在15(符合预期)
  • 慢SQLTOP10 :发现new_db某查询超时,优化索引后降至50ms
  • 切换成功率 :99.998%(仅2次因网络抖动失败,自动重试成功)
    注:监控配置模板私信"多数据源监控"获取

七、写在最后:技术人的尊严

多数据源不是炫技,而是对数据的敬畏

那次事故后,我在工位贴了张纸条:

"你敲下的每一行配置,都连着千万用户的信任"

如今带新人,第一课永远是:

1️⃣ 配置必加注释

2️⃣ 生产操作双人复核

3️⃣ 监控告警宁可误报,不可漏报


互动时间

💬 灵魂拷问

你在多数据源踩过最深的坑是什么?是事务失效?还是连接泄漏?

👉 评论区说出你的故事

👉 觉得干货? 点赞+收藏+关注三连!转发给那个总说"多数据源很简单"的同事(别问我是怎么知道的)

相关推荐
(>_<)13 小时前
java minio 分片上传工具类与测试demo
java·minio·分片上传
ZeroTaboo13 小时前
rmx:给 Windows 换一个能用的删除
前端·后端
Coder_Boy_13 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例
java·人工智能·spring boot·后端·spring
Victory_orsh13 小时前
AI雇佣人类,智能奴役肉体
后端
踢足球092913 小时前
寒假打卡:2026-2-7
java·开发语言·javascript
闻哥13 小时前
Kafka高吞吐量核心揭秘:四大技术架构深度解析
java·jvm·面试·kafka·rabbitmq·springboot
金牌归来发现妻女流落街头13 小时前
【Springboot基础开发】
java·spring boot·后端
考琪14 小时前
Nginx打印变量到log方法
java·运维·nginx
wangjialelele14 小时前
Linux中的进程管理
java·linux·服务器·c语言·c++·个人开发