多数据源下事务管理器配置错误导致数据回滚异常
本文是Spring Boot 生产配置实战系列的第二篇 叙事框架:
现象 → 排查过程 → 根因 → 修复 → 预防
问题现象
某日上午 10:30,客服群突然收到用户投诉——限时优惠活动下单失败,但积分却被扣了。

值班小A上服务器查看,CPU 才 22.5%,load average 2.45,系统资源正常,没有明显的告警。

看应用日志发现:createOrderAndFail 方法在插入订单记录、扣减积分后,因为库存不足抛了异常,事务触发了回滚。但回滚后数据是否真的一致?
排查过程
第一步:对比两个库的数据
翻应用日志确认了异常的发生时间,然后直接对比两个数据库:

结果触目惊心:
- order_db:
t_order表中没有 user_id=u002 的记录 ✅ 说明订单回滚成功了 - account_db:
t_user_points的 balance 从 2000 降到了 1800(扣了 200)❌ 积分没回滚 - account_db:
t_points明细表里有积分扣减记录(created_at 就是异常时间戳)
数据不一致:订单没创建成功,积分却被扣了 200。
第二步:查看数据源配置
项目配置了两个数据源:order_db(订单库)和 account_db(积分库)。

两个 @Configuration 各配了一个 DataSource + DataSourceTransactionManager。order_db 的是 @Primary。
第三步:Arthas 查事务管理器运行时
用 Arthas 的 vmtool 看运行时状态:

DataSourceTransactionManager@6a8f0c -> HikariDataSource (order_db)
DataSourceTransactionManager@3b9e45 -> HikariDataSource (account_db)
两个 TransactionManager 都在,各自管理不同的数据源。
再看 TransactionInterceptor 的默认事务管理器:
transactionManager=@DataSourceTransactionManager[order_db]
Spring 默认绑到了 orderTransactionManager。
第四步:确认 @Transactional 的绑定
查看代码:

@Service
public class OrderService {
@Transactional
public void createOrderAndFail(...) {
orderJdbcTemplate.update("INSERT INTO t_order ...");
pointsService.deductPoints(userId, 200);
if ("p999".equals(productId)) {
throw new RuntimeException("库存不足");
}
}
}
@Transactional 没有指定 transactionManager。Spring 默认注入 @Primary 的 orderTransactionManager,这个管理器只管理 order_db 的连接。
当 pointsService.deductPoints 写入 account_db 时,使用的是 account_db 的独立连接,不受 orderTransactionManager 管理。
所以异常发生时: - order_db ✅ 事务回滚 → 订单记录被清除 - account_db ❌ 不在事务中 → 积分扣减已提交,无法回滚
根因分析
两个关键因素:
1. @Transactional 默认只管理一个数据源
Spring 声明式事务通过 TransactionInterceptor 实现。当 @Transactional 不指定 transactionManager 或 value 时,Spring 会查找 @Primary 的 PlatformTransactionManager Bean。
在多数据源配置中,这意味着只有一个数据源的数据参与事务。另一个数据源的每次 JDBC 操作都是自动提交的。
2. 跨库操作没有在同一个事务管理范围内
OrderService.createOrderAndFail 的业务逻辑跨越了两个数据库:
- INSERT order(order_db)
- UPDATE/DELETE points(account_db)
但 @Transactional 只包裹了 order_db 的连接,account_db 的操作独立提交。这是伪事务——看起来有事务注解,实际上只管理了一半的数据。
修复方案
方案 A:ChainedTransactionManager(短期修复)
新增一个 ChainedTransactionManager,将两个 DataSourceTransactionManager 链起来:
@Configuration
public class ChainedTxConfig {
@Bean(name = "chainedTxManager")
public ChainedTransactionManager chainedTxManager(
@Qualifier("orderTransactionManager") PlatformTransactionManager orderTx,
@Qualifier("accountTransactionManager") PlatformTransactionManager accountTx) {
return new ChainedTransactionManager(orderTx, accountTx);
}
}
修改 OrderService,显式指定事务管理器:
@Transactional(transactionManager = "chainedTxManager")
public void createOrderAndFail(...) { ... }
ChainedTransactionManager 的工作原理:依次在多个 PlatformTransactionManager 上开启、提交或回滚事务。当某个管理器回滚时,之前已提交的管理器也会强制回滚(如果底层存储支持)。
方案 B:MQ 异步解耦(长期方案)
跨库事务本质上是分布式事务问题。ChainedTransactionManager 是同步协调,有性能和可靠性风险。更好的方案是:
下单 → 订单库写入(本地事务)→ 发 MQ 消息 → 积分服务消费 → 积分库扣减
积分扣减失败也不影响下单成功,通过 MQ 重试 + 对账补偿保证最终一致性。
方案 C:避免跨库事务
审视业务,看积分扣减是否真的需要和订单创建在同一个事务里。如果积分扣减失败可以后续补偿,就不应该用强事务耦合。
验证结果
修复后部署验证:

再跑同样的失败场景: - order_db 回滚 ✅ - account_db 也回滚了 ✅(ChainedTransactionManager 协调回滚) - balance 恢复到 2000,数据一致 ✅
INFO OrderService : orderDb rolled back, accountDb rolled back too
INFO OrderService : cross-db rollback verified: data consistent
避坑建议
1. 多数据源项目必须显式指定 transactionManager
任何时候在 Service 上写 @Transactional,如果项目存在多个 PlatformTransactionManager,必须显式指定:
// ❌ 错误:默认找 @Primary,可能不是你想要的
@Transactional
// ✅ 正确:显式指定
@Transactional(transactionManager = "chainedTxManager")
// 或明确指定名称
@Transactional("chainedTxManager")
2. @Primary 不等于「所有事务都用它」
@Primary 只是告诉 Spring「有歧义时默认注入这个」。它解决的是依赖注入的歧义,而不是声明式事务的绑定——TransactionInterceptor 默认确实会取 @Primary 的 TransactionManager,但这个默认行为在多数据源场景下几乎总是错的。
3. 测试必须覆盖异常回滚路径
这个 Bug 的伏笔:
- 开发环境单库(H2),不会出现多数据源事务问题
- 预发布环境测试只测了正向流程(下单成功),没测异常回滚
- 监控没配跨库一致性检查,数据不一致靠客服投诉发现
建议: - 集成测试必须覆盖「业务异常 → 事务回滚 → 数据一致」的断言 - 多数据源项目应加数据对账任务,定期检查跨库数据一致性
4. 三思而「跨库」
每次增加数据源、跨库操作时,先问几个问题: - 这个操作真的需要跨两个库吗? - 能否通过数据冗余、同步、MQ 解耦避免跨库? - 如果一定要跨库,用 Seata / ShardingSphere 等分布式事务框架,还是用 ChainedTransactionManager?
ChainedTransactionManager 本质上还是本地事务的链式组合,不是真正的 XA 分布式事务。它依赖底层资源的回滚能力,在极端场景下(如网络分区、进程崩溃后重启)仍有不一致风险。
附:完整命令清单
数据对比
# 查订单库(当前用户)
docker exec -i order-mysql mysql -uorder_app -p******** order_db \
-e 'SELECT * FROM t_order WHERE user_id="u002"'
# 查积分库(被误扣了)
docker exec -i account-mysql mysql -uaccount_app -p******** account_db \
-e 'SELECT * FROM t_user_points WHERE user_id="u002"'
# 查积分扣减明细
docker exec -i account-mysql mysql -uaccount_app -p******** account_db \
-e 'SELECT * FROM t_points WHERE user_id="u002"'
Arthas 运行时诊断
# 查看所有 DataSourceTransactionManager 及其绑定的数据源
vmtool --action getInstances \
--className org.springframework.jdbc.datasource.DataSourceTransactionManager \
--express 'instances.{#this.toString()+" -> "+#this.dataSource}' -x 2
# 查看 TransactionInterceptor 默认绑定的事务管理器
vmtool --action getInstances \
--className org.springframework.transaction.interceptor.TransactionInterceptor \
--express 'instances.{#this.transactionManager}' -x 2
# 搜索所有 TransactionManager Bean
sc -d *TransactionManager
配置检查
# 搜索 TransactionManager 和 @Transactional 的使用
grep -rn 'TransactionManager\|@Transactional' src/main/java/
# 查看当前 datasource 配置
cat src/main/resources/application.yml
发送测试请求
# 正常下单(两个库都成功)
curl -XPOST http://localhost:8080/api/order/create \
-H 'Content-Type: application/json' \
-d '{"userId":"u001","productId":"p001","amount":99}'
# 触发失败场景(验证回滚一致性)
curl -XPOST http://localhost:8080/api/order/create-fail \
-H 'Content-Type: application/json' \
-d '{"userId":"u002","productId":"p999","amount":199}'
# 检查订单表
curl http://localhost:8080/api/order/check-orders
# 检查积分余额
curl "http://localhost:8080/api/order/check-points?userId=u002"
📖 完整版带可复现 Demo → opencao.cn 📺 公众号「Ai拆代码的曹操」 🌟 知识星球「源阅会」(82877104)