★ @Transactional 在哪些场景下会失效?如何避免?
结论先行:@Transactional 依赖 Spring AOP 代理生效,凡是“没走代理、异常没抛出去、事务没接管”都会失效。高频场景可归纳为六类:同类自调用、方法非 public、异常被 catch 吞掉、只抛检查异常(默认不回滚)、事务入口/传播配错、跨线程调用。定位口诀:先问“有没有走代理?异常抛出去了吗?异常类型在不在回滚范围里?”
失效场景对照表
| 场景 | 失效原因 | 解决办法 |
|---|---|---|
| 同类 this 自调用 | 调用发生在原始对象内部,绕过代理 | 注入自身代理 / 拆 Bean / AopContext |
| 方法非 public | AOP 代理默认只拦截 public 方法 | 改为 public 并由外部走代理入口 |
| catch 后异常没抛 | 事务感知不到异常发生 | 抛运行时异常或编程式回滚 |
| 只抛检查异常 | 默认只回滚 RuntimeException/Error | rollbackFor = Exception.class |
| 事务入口/传播配错 | 事务没在预期边界开启 | 检查调用入口与 propagation |
| 新线程/异步调用 | 事务连接绑定当前线程 | 事务不跨线程;另起事务或外层提交后处理 |
自调用失效示例
@Service
public class PayService {
public void pay() {
deduct(); // this.deduct() 未经过代理对象 → 事务注解不生效
}
@Transactional(rollbackFor = Exception.class)
public void deduct() { /* 扣款逻辑 */ }
}
正确姿势:拆 Bean 走代理
@Service
public class PayFacade {
@Autowired
private PayService payService; // 注入另一个 Bean,拿到的是代理
public void pay() {
payService.deduct(); // 调用发生在代理上,事务生效
}
}
异常处理三选一:不 catch 让它抛出去;catch 后 rethrow RuntimeException;或用 TransactionTemplate 手动控制提交回滚。
常见追问 / 记忆点
- 追问:同类自调用怎么保事务?答:注入自身代理(@Lazy 自引用)、AopContext.currentProxy(),或干脆拆成两个 Bean。
- 追问:catch 了业务异常但确实想回滚怎么办?答:catch 里 rethrow RuntimeException,或调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
- 记忆点:失效三查——“走代理了吗?异常抛了吗?异常在回滚白名单吗?”