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

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


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

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

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

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

<!-- 核心:必须指定版本!避免与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配置(生产级模板)

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)

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

// 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>

事务切换真实案例

// 错误写法:事务内切换,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主动加我微信:“下次迁移还找你!”

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

(文字描述监控效果)


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

多数据源不是炫技,而是对数据的敬畏
那次事故后,我在工位贴了张纸条:

如今带新人,第一课永远是:
1️⃣ 配置必加注释
2️⃣ 生产操作双人复核
3️⃣ 监控告警宁可误报,不可漏报


互动时间

 灵魂拷问
你在多数据源踩过最深的坑是什么?是事务失效?还是连接泄漏?
评论区说出你的故事
 觉得干货? 点赞+收藏+关注三连!转发给那个总说“多数据源很简单”的同事(别问我是怎么知道的)

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com