多数据源事务:@Transactional 只管了一个库
场景:@Transactional 嵌套调用导致数据半改——一个注解管不到两个数据库连接 路径:现场 → 三层边界拆解(JDBC/传播/数据源)→ 处方 → 雷标
上篇文章讲了同一个类里方法调方法不走代理导致 @Transactional 不生效——这是"代理绕过"的问题。这篇我们更进一步:就算走了代理、@Transactional 正常触发了,它到底管了多少?
你以为 @Transactional 会管所有数据源?它只管理了一个 JDBC Connection 的 begin/commit/rollback。同一个 DataSource 上开的新 Connection 它也不管。不是配置错了——是 Annotation 本身的设计边界。
三年前接到的那个工单,让我第一次把这个边界看清楚。
【现场】—— 一个"管不到"的案例
运营群突然炸了:"下单失败率 23%,用户说钱扣了但订单没生成,已投诉到客服。"
我登录服务器,P99 从 200ms 飙到 3.2s,日志滚屏全是 Connection is not available, request timed out。

第一反应——连接池不够了。但查 HikariCP 监控:active: 20, max: 20,确实满了。20 个连接全占着,一个不放。
这就奇怪了——接口 QPS 才 50,平均查询耗时 30ms,按公式算 4-6 个连接就够了,怎么会吃掉 20 个?
再往下翻日志,发现一个更诡异的现象——同一个请求,A 操作成功了,B 操作却回滚了:
2026-06-27 10:23:15.672 [http-nio-8080-exec-3] INFO o.x.ServiceA - outer: order_id=ORD20260627001 created
2026-06-27 10:23:15.890 [http-nio-8080-exec-3] INFO o.x.ServiceB - inner: inventory locked for ORD20260627001
2026-06-27 10:23:16.102 [http-nio-8080-exec-3] ERROR o.x.ServiceA - outer failed: payment timeout, rolling back...
2026-06-27 10:23:16.110 [http-nio-8080-exec-3] INFO o.x.ServiceA - outer rolled back: order_id=ORD20260627001
订单回滚了,库存扣减还在:SELECT inventory WHERE order_id='ORD20260627001' → locked ❌
外层方法有 @Transactional,内层方法也有 @Transactional(propagation = Propagation.REQUIRES_NEW)。两个注解,一个回滚一个提交。

"有 @Transactional 罩着,怎么会只回滚一半?"
而且——20 个连接怎么全被占住了?谁都不放?
【拆解】—— @Transactional 的三层边界
Spring 声明式事务(通过 AOP 代理拦截方法调用、自动管理事务开启提交回滚的机制)的核心里面,@Transactional 管理的不是"业务逻辑",是一个 JDBC Connection。
第一层:JDBC 边界——@Transactional = Connection 管理
TransactionInterceptor(Spring AOP 事务拦截器)拦截带 @Transactional 的方法后,委托 DataSourceTransactionManager(基于 JDBC 连接的数据源事务管理器)执行实际事务操作。它的 doBegin 方法做的事并不神秘:

它拿一个 Connection,关掉 autoCommit,完事。@Transactional 管理的就是这个连接的 begin → commit / rollback。
但有一个关键细节:doBegin 拿到的 Connection 不是凭空来的。Spring 在 TransactionSynchronizationManager(线程级事务同步管理器)里维护了一个 resources 映射——Map<DataSource, ConnectionHolder>。每个 DataSource 绑一个 ConnectionHolder,后者包裹一个 JDBC Connection。
当 @Transactional 方法开始,DataSourceTransactionManager 做的第一件事就是从 TransactionSynchronizationManager.resources 里查当前线程有没有这个 DataSource 的 ConnectionHolder:
// DataSourceTransactionManager.doGetTransaction()
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
ConnectionHolder conHolder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
有 → 复用;没有 → 从连接池拿一个新的,bind 到线程上。
回头再看截图里的内外层调用:
ServiceA.createOrder() → @Transactional → doBegin on Connection_A
↓
ServiceB.lockInventory() → @Transactional(REQUIRES_NEW)
→ 挂起 Connection_A(从线程解绑但保留)
→ 从连接池拿 Connection_B → doBegin
→ 提交 → 解绑 Connection_B → 恢复 Connection_A
两个方法各拿一个 Connection,各管各的 begin/commit/rollback。外层回滚不影响内层已提交的 Connection。
关键问题来了:同一次调用为什么会有两个连接? 而且挂起的 Connection_A 一直没有归还连接池——这就是 20 个连接被吃满的直接原因。
第二层:传播边界——REQUIRES_NEW 开了新连接
Propagation.REQUIRES_NEW(事务传播级别——挂起当前事务、开启全新独立事务)的语义决定了:
外层事务(Connection_A)→ 挂起 → 开新连接(Connection_B)→ 新事务 → 提交 → 恢复外层
Spring 在 AbstractPlatformTransactionManager(平台事务管理器抽象基类)的实现里,对 REQUIRES_NEW 的处理是:

挂起当前 Connection → 从连接池拿一个新 Connection → 在新连接上 doBegin。
这个连接的独立性解释了"为什么外层回滚不影响内层"——因为它们操作的数据库连接根本就不是同一个。
第三层:数据源边界——不同 DataSource 更管不到
理解了这个机制,多数据源的问题就自然清楚了:

两个配置各创建了一个 DataSourceTransactionManager,各持一个 DataSource。@Transactional 不指定 transactionManager 时,默认查 @Primary 的 orderTransactionManager。account 的操作使用 accountDataSource 的连接,这个连接完全不在 orderTransactionManager 的管理范围。

【处方】—— 理解边界后的正确选择
不是什么奇怪 Bug,是你默认了它不应该默认的事。
最危险的 @Transactional 不是写漏了,是你以为它能跨库管。
不是 @Transactional 不够好,是 Connection 没有分身术。理解这个边界后,不同场景选不同工具:
| 边界类型 | 问题 | 方案 |
|---|---|---|
| 单数据源、默认传播 | — | @Transactional 够用 |
| 单数据源、REQUIRES_NEW | 内层事务独立提交 | 确认传播语义是否必要;否则用 REQUIRED 参与外层事务 |
| 多数据源 | 事务只管理了主库 | ChainedTransactionManager 链式协调 |
| 跨资源类型(Redis/MQ) | @Transactional 不感知 | 最终一致 / TCC / 消息事务 |
三条铁的规则
规则 1:多数据源必须显式指定 transactionManager
// ❌ 错误:没指定,默认取 @Primary,管不到第二个库
@Transactional
public void createOrderAndDeduct() { ... }
// ✅ 正确:显式指定链式事务管理器
@Transactional("chainedTxManager")
public void createOrderAndDeduct() { ... }
链式管理器配置(ChainedTransactionManager 来自 Spring Data,按顺序依次在每个 DataSourceTransactionManager 上开启或回滚事务):
@Configuration
public class ChainedTxConfig {
@Bean("chainedTxManager")
public PlatformTransactionManager chainedTxManager(
@Qualifier("orderTxManager") PlatformTransactionManager orderTx,
@Qualifier("accountTxManager") PlatformTransactionManager accountTx) {
return new ChainedTransactionManager(orderTx, accountTx);
}
}
规则 2:REQUIRES_NEW 要确认"外层回滚时内层需不需要回滚"
需要 → 用 REQUIRED 替代,让内层参与外层事务。不需要 → 自己接受数据不一致的可能性,业务上做好补偿。
如果保留 REQUIRES_NEW,必须评估对连接池的压力:每层嵌套多持一个连接。maxPoolSize = 并发数 × (1 + 最大嵌套层数) 才够。
规则 3:REQUIRED 传播下,隐式指定 @Primary 就是给自己埋坑
// 多数据源项目,写 @Transactional 时每次都要问:
// "这个 @Transactional 管了哪个 DataSource?"
// 不加参数 = @Primary = 可能不是你想要的
@Transactional("specificTxManager")
如何验证当前 @Transactional 管了哪些连接
用 Arthas 查 TransactionSynchronizationManager 当前线程绑定的所有资源:
# Arthas:看当前线程的事务绑定状态
vmtool --action getInstances \
--className org.springframework.transaction.support.TransactionSynchronizationManager \
--express 'instances.{resources.get(#this).toString()}' -x 3
# 单数据源正常时:
# {[HikariDataSource (order_db)]=ConnectionHolder{conn=HikariProxyConnection@1234}}
# ✅ 只绑定了一个 DataSource 的一个 Connection
# REQUIRES_NEW 嵌套时有两个连接绑定到同一个 DataSource:
# {[HikariDataSource (order_db)]=ConnectionHolder{conn=A@5678},
# [HikariDataSource (order_db)]=ConnectionHolder{conn=B@9012}}
# ⚠️ 两个连接在同一个线程上,但各管各的事务
动手验证:最小复现 Demo
@SpringBootTest
class TransactionBoundaryTest {
@Autowired private JdbcTemplate jdbc;
@Autowired private PlatformTransactionManager txManager;
@Test
void testRequiresNewUsesSeparateConnection() {
// 外层:开启事务
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
// 写一条数据
jdbc.execute("INSERT INTO demo VALUES ('outer')");
// 内层:REQUIRES_NEW(独立新连接)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus inner = txManager.getTransaction(def);
jdbc.execute("INSERT INTO demo VALUES ('inner')");
txManager.commit(inner); // inner 提交
txManager.rollback(outer); // outer 回滚
// 结果:demo 表中只有 'inner' 一条记录
// 'outer' 被回滚了,但 'inner' 因为是独立连接提交的,不受影响
}
}
【雷标】—— 🔴 在你的项目里搜这些
打开你的项目,Ctrl+Shift+F:
@Transactional→ 检查所有不带transactionManager/value的方法,确认涉及的 DataSource 只有一个Propagation.REQUIRES_NEW→ 检查外层如果回滚了,内层已提交的数据是否有补偿DataSourceTransactionManager→ 有几个?谁加了@Primary?谁没被任何@Transactional引用?
下篇我们聊一个更隐蔽的场景:@Async 异步方法也没走代理——你以为它在异步执行,其实它在同步阻塞你的接口。