灵魂之桥前传:追忆
83.49M · 2026-02-09
这不是演习。去年负责省级政务系统迁移时,因多数据源配置疏漏,导致旧库被误写入测试数据,紧急回滚3小时。从此我立下flag:多数据源,必须吃透!
| 方案 | ShardingSphere | 手动配置AbstractRoutingDataSource | dynamic-datasource(推荐) |
|---|---|---|---|
| 学习成本 | 高(需理解分片规则) | 极高(重写数据源路由逻辑) | 低(注解即用) |
| 动态扩展 | 需重启 | 需重启 | 运行时动态增删 |
| 事务支持 | 复杂 | 易出错 | @DS与@Transactional完美兼容 |
| 踩坑指数 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 我的选择 | 迁移场景不需要分片 | 通宵写路由逻辑后崩溃 | 30分钟搞定核心配置 |
<!-- 核心:必须指定版本!避免与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 datasourcespring:
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
生产加固点:
read-only: true 为从库上“保险栓”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数据!)| 坑点 | 现象 | 解决方案 |
|---|---|---|
| 事务内切换失效 | @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周)
@DS + AOP自动双写(代码略,私信可发)校验阶段(3天)
SELECT COUNT(*) FROM user WHERE update_time > '昨日'切读阶段(凌晨2点)
primary: new_db,重启服务下线旧库(1周后)
成果:
(文字描述监控效果)
多数据源不是炫技,而是对数据的敬畏。
那次事故后,我在工位贴了张纸条:
如今带新人,第一课永远是:
1️⃣ 配置必加注释
2️⃣ 生产操作双人复核
3️⃣ 监控告警宁可误报,不可漏报
灵魂拷问:
你在多数据源踩过最深的坑是什么?是事务失效?还是连接泄漏?
评论区说出你的故事
觉得干货? 点赞+收藏+关注三连!转发给那个总说“多数据源很简单”的同事(别问我是怎么知道的)