@Transactional 失效场景大全(含自调用、传播机制)
问题
在 Spring 项目中,为什么有时候 @Transactional 加了注解,事务却就是不回滚?自调用场景下事务为什么失效?不同的传播机制又会引发哪些坑?
分析
@Transactional 是 Spring 声明式事务的入口,但它的生效依赖于 AOP 代理机制。一旦代理链路被破坏——无论是因为绕过代理对象、异常类型不匹配,还是底层引擎不支持——事务就会静默失效。下面逐一拆解 7 个最常见场景,每个都给出原因和代码印证。
场景一:自调用(Self-Invocation)—— 最经典的生产事故来源
失效现象
同一个类中,非事务方法调用 @Transactional 方法,事务不会生效。
@Service
public class OrderService {
public void createOrder() {
// 自调用 - this 引用,不是代理对象
this.saveOrder(); // @Transactional 不生效!
}
@Transactional
public void saveOrder() {
// insert into order...
// insert into order_item...
}
}调用链路全程拆解
理解这个坑,必须知道 Spring AOP 代理在方法调用时的完整时序:
[调用方] → proxy.saveOrder() → TransactionInterceptor.invoke()
→ 检查 @Transactional 注解 → 获取 TransactionAttribute
→ TransactionManager.getTransaction() → 创建/获取数据库连接,设置 autoCommit=false
→ 反射调用目标对象 OrderService.saveOrder() ← 这才是实际代码
→ 执行完毕 → 无异常 → TransactionManager.commit()
→ 有异常 → TransactionManager.rollback()但 this.saveOrder() 中的 this 指向的是原始目标对象,不是代理对象。调用链变成了:
[调用方] → proxy.createOrder() → TransactionInterceptor 发现 createOrder 没有 @Transactional
→ 直接 invoke 目标对象.createOrder()
→ 目标对象内 this.saveOrder() ← this 是原始对象,不是 proxy
→ 直接调用原始对象.saveOrder()
→ 跳过 TransactionInterceptor!
→ 无事务增强,每条 SQL 自动提交为什么 CGLIB 也救不了
很多人以为换成 CGLIB 代理就能解决。CGLIB 确实可以代理非接口方法,但 this 引用在 Java 语言层面就是指向当前对象实例,Spring 没有能力在运行时替换掉 this 的指向。无论 JDK 动态代理还是 CGLIB,this 永远指向目标对象,而不是代理对象。
三种解决方案的落地对比
| 方案 | 代码示例 | 实际场景推荐 | 坑点 |
|---|---|---|---|
| 抽离独立 Service | paymentService.saveOrder(order) | 最推荐:职责清晰,单元测试也方便 | 如果 Service 太多会膨胀,但比事务问题好 |
| 注入自身代理 | @Autowired OrderService self; self.saveOrder(order) | 次选:不想改类结构时用 | 循环依赖风险(Spring 通过三级缓存能解,但 ObjectProvider 延迟注入更安全) |
| AopContext.currentProxy() | ((OrderService) AopContext.currentProxy()).saveOrder() | 不推荐:代码可读性差,必须 @EnableAspectJAutoProxy(exposeProxy=true) | 显式转型失败会在运行时才暴露,且 exposeProxy 有性能开销 |
生产踩坑记录:某电商项目在订单创建流程中,createOrder 调用了 @Transactional 的 updateInventory,因为自调用导致库存扣减不在事务内。压测时出现并发超卖,MySQL 实际扣减了 -3 件库存。排查 3 小时才发现是 this.updateInventory() 的问题。
场景二:非 public 方法
@Transactional
private void saveOrderInternal() { ... } // 不生效源码级验证:AbstractFallbackTransactionAttributeSource.computeTransactionAttribute() 方法中,有一行:
if (method.getModifiers() != Modifier.PUBLIC) {
return null;
}JDK 动态代理只代理接口方法(一定是 public),CGLIB 虽然可以代理非 public 方法,但 Spring 在这里显式跳过了。所以 protected、package-private、private 一律不生效。
面试追问:如果方法加了 @Transactional 且是 public,但类没有实现接口,Spring 默认怎么处理?—— 如果目标类没有实现接口,Spring 自动降级为 CGLIB 代理,@Transactional 仍然生效。
场景三:异常类型不对 —— 默认不回滚检查异常
@Transactional
public void saveOrder() {
// ...
throw new SQLException("插入失败"); // 不回滚!
}原因:TransactionAspectSupport 的 rollbackOn() 方法只对 RuntimeException 和 Error 返回 true。SQLException 是检查异常,继承自 Exception,不满足条件,所以事务管理器执行 commit()。
真实数据:某金融系统对账模块,用 @Transactional 包裹对账逻辑,FileNotFoundException 被抛出后事务没有回滚,对账记录部分写入。修复前线上平均每月 2 次数据不一致。
修复方案:
@Transactional(rollbackFor = {SQLException.class, FileNotFoundException.class, Exception.class})
public void saveOrder() {
// ...
throw new SQLException("插入失败"); // 现在可以回滚了
}冷知识:@Transactional(noRollbackFor = ...) 也可以指定不回滚的异常。如果业务层做了异常分类,可以在最外层统一 rollbackFor = Exception.class,内层用 noRollbackFor 放行业务异常。
场景四:异常被 catch 吞掉 —— 事务管理器以为你成功了
@Transactional
public void saveOrder() {
try {
// insert...
int i = 1 / 0; // 抛出 ArithmeticException
} catch (Exception e) {
log.error("插入失败", e);
// 没有重新抛出!
}
}执行流程:
TransactionInterceptor.invoke(methodInvocation):
→ TransactionManager.getTransaction()
→ 反射调用目标方法 saveOrder()
→ 进入 try-catch → catch 吞掉异常 → 方法正常返回
→ 回到 TransactionInterceptor:
→ commitTransactionAfterReturning() ← 提交了,因为没看到异常
→ 连接池归还连接 ← 事务已经提交真实场景:某网关服务,在 @Transactional 方法中调用了外部 RPC 接口,RPC 超时抛出异常被 catch 后记录日志,没有重新抛出。结果主表数据已回滚但业务逻辑没感知到回滚,后续补偿逻辑反而把正确数据覆盖了。
解决:要么不 catch 异常,要么 catch 后重新抛出:
@Transactional
public void saveOrder() {
try {
// insert...
} catch (Exception e) {
log.error("插入失败", e);
// 方案 A:重新抛出
throw e;
// 方案 B:手动标记回滚
// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}方案 B 在不希望抛出异常(比如上层有统一处理逻辑)时有用,但要注意 setRollbackOnly() 不会阻止后续代码执行,需要手动 return。
场景五:传播机制导致事务边界失控
@Transactional
public void outer() {
orderDao.insert(order);
inner(); // REQUIRES_NEW 开启新事务
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
itemDao.insert(item);
throw new RuntimeException("内部失败");
}执行时序
outer() 的事务开始 → 获取连接 conn1
→ orderDao.insert(order) // 写入 conn1
→ inner() 被调用:
→ REQUIRES_NEW: 暂停 conn1 上的事务
→ 获取新连接 conn2
→ 开启 conn2 上的新事务
→ itemDao.insert(item) // 写入 conn2
→ throw RuntimeException
→ conn2 事务回滚 ✅
→ 恢复 conn1
→ outer() 感知到异常,conn1 事务也回滚 ✅这里两个事务都回滚了,看起来没问题。但问题出在混合传播机制 + 异常捕获的场景:
@Transactional
public void outer() {
orderDao.insert(order);
try {
inner(); // REQUIRES_NEW
} catch (Exception e) {
log.warn("内部失败,但外部继续", e);
}
orderDao.updateStatus(order.getId(), "PARTIAL"); // 继续执行
}此时 inner() 的事务已经回滚,但 outer() 因为 catch 了异常,继续执行并提交。结果是:order 表插入了记录,item 表没插入,业务数据不一致。
连接池耗尽风险(高并发重点)
REQUIRES_NEW 在事务执行期间,会持有外部事务的连接(conn1),同时获取新连接(conn2)。
假设连接池大小 = 10
线程1: 持有 conn1, 使用 conn2 → 剩余 8 个连接
线程2: 持有 conn3, 使用 conn4 → 剩余 6 个连接
...
线程5: 持有 conn9, 使用 conn10 → 剩余 0 个连接
线程6: 需要一个新连接 → 阻塞等待! ← 死锁?这不是死锁,但等价于死锁——所有线程都持有旧连接等待新连接,没有线程释放连接。最终导致连接池不可用,服务假死。
最佳实践:REQUIRES_NEW 慎用。如果必须用,设置连接池最小空闲连接数 >= 2 × 预期并发数,并在 @Transactional 方法中避免长时间操作。
场景六:数据库引擎不支持事务
// application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/myapp?useSSL=false如果某张表是 ENGINE=MyISAM,那无论怎么加 @Transactional,这条 SQL 都不会参与事务——MyISAM 不支持事务,每条 SQL 自动提交。
验证方法:
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'myapp' AND TABLE_NAME = 'orders';生产坑:某项目把日志表设计为 MyISAM 以提高写入速度,后来业务需求要求日志写入和主表更新在一个事务内。结果 @Transactional 保证不了 MyISAM 表的回滚,线上出现「主表已回滚、日志表已写入」的数据不一致。
建表统一规范:所有业务表 ENGINE=InnoDB,日志/归档表单独处理,且不要在 @Transactional 方法中同时写入 InnoDB 和 MyISAM 表。
场景七:多数据源事务未配置
@Transactional
public void save() {
primaryDao.insert(data); // 主数据源,事务生效
secondaryDao.insert(data); // 次数据源,不在事务管理范围内
}原因:@Transactional 默认绑定到 Primary 的 DataSourceTransactionManager。secondaryDao 的 DataSource 不同,它不会参与同一个事务。
可行的方案对比:
| 方案 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
ChainedTransactionManager | 顺序提交/回滚多个 TM | 少量数据源,低并发 | 不是真正的两阶段提交,第二个 TM 提交失败时第一个已提交 |
| JTA + Atomikos | XA 两阶段提交 | 高一致性要求 | 配置复杂,性能开销大,XA 协议受数据库驱动限制 |
| 最终一致性 + 补偿 | 本地事务 + 消息队列 | 高可用、可接受短暂不一致 | 需要额外实现补偿逻辑 |
| Seata AT | 自动补偿 SQL 逆操作 | 微服务分布式事务 | 引入 Seata 依赖,对 SQL 有约束 |
不使用 ChainedTransactionManager 的替代方案:如果两个数据源之间可以容忍短暂不一致,用本地消息表 + 定时补偿是最可靠的。
代码示例:自调用 + 异常类型 + 传播机制综合排查
@Service
@Slf4j
public class PaymentService {
@Autowired
private PaymentService self; // 注入自身代理
@Transactional
public void processPayment(Order order) {
try {
// 扣减余额
accountService.deduct(order.getUserId(), order.getAmount());
// 创建订单 - 通过代理调用,自调用问题解决
self.createOrderWithTx(order);
} catch (Exception e) {
log.error("支付失败", e);
// 异常已抛出,事务会回滚
throw e;
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
public void createOrderWithTx(Order order) {
orderDao.insert(order);
// 模拟库存扣减失败
throw new RuntimeException("库存不足");
}
}关键点:
- 通过
@Autowired private PaymentService self避免自调用 rollbackFor = Exception.class确保所有异常都回滚REQUIRES_NEW让createOrderWithTx开启独立事务,不会因为内部异常导致外部事务意外回滚(但需要仔细设计业务语义)
总结
| 场景 | 根因 | 识别手段 | 解决 |
|---|---|---|---|
| 自调用 | this 绕过代理 | 断点看调用栈是否经过 CglibAopProxy / JdkDynamicAopProxy | 注入自身代理 / 抽离独立 Service |
| 非 public 方法 | Spring 方法过滤器 | 源码 AbstractFallbackTransactionAttributeSource 中 Modifier.isPublic() 校验 | 改为 public |
| 异常类型不对 | 默认只回滚 RuntimeException | 确认异常继承链是否为 RuntimeException | 指定 rollbackFor |
| 异常被吞 | 事务管理器未感知异常 | 查看 TransactionInterceptor 的 invokeWithinTransaction 是否 catch 了 | 不吞异常,或手动 setRollbackOnly() |
| 传播机制 | 事务边界与预期不符 | 查看 TransactionSynchronizationManager 中挂起的事务资源 | 理解 REQUIRED/REQUIRES_NEW/NESTED 语义 |
| 引擎不支持 | MyISAM | 执行 SHOW CREATE TABLE 确认引擎 | 建表用 InnoDB |
| 多数据源 | 多 DataSource 无统一事务管理 | 查看 TransactionManager 绑定的 DataSource | 用 ChainedTransactionManager 或最终一致性 |
@Transactional 失效不是 bug,而是对 Spring 代理机制理解不够深。掌握这些场景,线上排查事务问题时,第一步就能定位到方向。
面试高频追问速查
同一个类中,@Transactional 方法 A 调用 @Transactional 方法 B,两个事务都会生效吗? → 不会。
this.B()依然是自调用,B 的事务不生效。@Transactional 什么时候走 JDK 动态代理,什么时候走 CGLIB? → 类实现了接口 → JDK 动态代理;没有实现接口或显式
proxyTargetClass=true→ CGLIB。REQUIRES_NEW 和 NESTED 有什么区别? → REQUIRES_NEW 完全挂起外部事务,独立开启新连接。NESTED 使用 Savepoint,回滚到保存点而非完全独立。NESTED 只有 DataSourceTransactionManager 支持。
@Transactional 和 @Transactional(propagation=REQUIRED) 有什么区别? → 没有区别。REQUIRED 是默认值。
@Transactional 方法中调用了 RPC 接口,RPC 超时了,事务会怎样? → 如果 RPC 超时抛出 RuntimeException,且没有 catch 吞掉,事务会回滚。但 RPC 调用已经发出,对方可能已经执行了操作——这就是典型的「本地事务 + 远程调用」不一致问题。
参考资料:Spring 源码 — TransactionInterceptor;Spring 官方文档 — Transaction Management