作者:不想打工的码农
原创声明:本文基于政务系统数据迁移真实项目,所有配置、监控截图、故障复盘均脱敏处理,拒绝"复制粘贴式教学"
一、血泪开场:那个被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
✨ 生产加固点:
- 密码绝不硬编码!对接Apollo/Nacos配置中心
read-only: true为从库上"保险栓"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周)
- 所有写操作同时写old_db + new_db
- 用
@DS+ AOP自动双写(代码略,私信可发) - 监控重点:new_db写入延迟、数据一致性校验
-
校验阶段(3天)
- 每日跑对比脚本:
SELECT COUNT(*) FROM user WHERE update_time > '昨日' - 差异数据自动修复(附校验脚本核心逻辑)
- 每日跑对比脚本:
-
切读阶段(凌晨2点)
- 修改
primary: new_db,重启服务 - 灰度策略:先切10%流量,观察1小时监控
- 修改
-
下线旧库(1周后)
- 确认无异常后,注释old_db配置
- 保留旧库30天(防回滚)
✅ 成果:
- 迁移期间0故障,用户无感知
- DBA主动加我微信:"下次迁移还找你!"
六、监控大屏:让风险看得见
(文字描述监控效果)
登录Grafana大屏:
- 连接池水位:old_db活跃连接稳定在5,new_db在15(符合预期)
- 慢SQLTOP10 :发现
new_db某查询超时,优化索引后降至50ms- 切换成功率 :99.998%(仅2次因网络抖动失败,自动重试成功)
注:监控配置模板私信"多数据源监控"获取
七、写在最后:技术人的尊严
多数据源不是炫技,而是对数据的敬畏 。
那次事故后,我在工位贴了张纸条:
"你敲下的每一行配置,都连着千万用户的信任"
如今带新人,第一课永远是:
1️⃣ 配置必加注释
2️⃣ 生产操作双人复核
3️⃣ 监控告警宁可误报,不可漏报
互动时间
💬 灵魂拷问 :
你在多数据源踩过最深的坑是什么?是事务失效?还是连接泄漏?
👉 评论区说出你的故事
👉 觉得干货? 点赞+收藏+关注三连!转发给那个总说"多数据源很简单"的同事(别问我是怎么知道的)