Spring 事务失效可能是哪些原因?
结论先行:@Transactional 要生效,必须同时满足四个条件——方法是外部调用打到代理上、方法可被代理覆写、异常能被事务拦截器看到且匹配回滚规则、底层存储真的支持事务。任何一条断掉就失效。线上排查先问三句:这个方法是被别的 Bean 调的吗?抛的异常是运行时异常吗?表引擎是 InnoDB 吗?
一、失效清单与判定顺序
| # | 场景 | 根因 | 修复 |
|---|---|---|---|
| 1 | 同类方法自调用 | 未经过代理 | 拆 Bean、自注入、AopContext |
| 2 | 方法非 public(Spring 6 前) | CGLIB/JDK 无法拦截 | 改 public |
| 3 | 方法 final/static/类 final | 无法覆写 | 去掉 final,或改为可代理方法 |
| 4 | 异常被 try-catch 吞掉 | 拦截器没收到异常 | catch 后 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 或重新抛出 |
| 5 | 抛受检异常 | 默认只回滚 RuntimeException 与 Error | @Transactional(rollbackFor = Exception.class) |
| 6 | Bean 不是 Spring 管理 | 没有代理 | 加 @Service/交给容器 |
| 7 | 存储不支持事务 | 如 MyISAM、部分 NoSQL | 换 InnoDB 或改用补偿 |
| 8 | 异步/多线程内执行 | 事务绑定 ThreadLocal | 异步任务内独立开启事务 |
| 9 | 传播行为为 NOT_SUPPORTED/NEVER | 主动不开事务 | 检查注解配置 |
| 10 | 未开启事务管理 | 缺少 @EnableTransactionManagement(Boot 自动配置一般已开) | 补注解、确认 PlatformTransactionManager 存在 |
| 11 | 切点被覆盖 | 自定义 AOP 切面 @Order 或代理模式冲突 | 调整顺序,确保事务拦截器在链上 |
二、最典型的三类
@Service
public class PayService {
// 失效 1:自调用。this.refund() 不走代理
@Transactional(rollbackFor = Exception.class)
public void payAndRefund() {
this.refund();
}
@Transactional(rollbackFor = Exception.class)
public void refund() { }
// 失效 2:异常被吞。拦截器认为方法正常结束,直接提交
@Transactional(rollbackFor = Exception.class)
public void withCatch() {
try {
orderMapper.insert(new Order());
int x = 1 / 0;
} catch (Exception e) {
log.error("插入失败", e); // 吞掉后事务已提交
}
}
// 失效 3:受检异常。默认规则只认 RuntimeException / Error
@Transactional
public void ioTask() throws IOException {
fileMapper.insert(new FileRecord());
throw new IOException("磁盘满"); // 不会回滚
}
}
三、正确写法
@Service
public class PayService {
private final RefundService refundService; // 拆到独立 Bean
public PayService(RefundService refundService) {
this.refundService = refundService;
}
@Transactional(rollbackFor = Exception.class) // 显式声明受检异常也回滚
public void payAndRefund() {
orderMapper.insert(new Order());
refundService.refund(); // 跨 Bean,必过代理
}
@Transactional(rollbackFor = Exception.class)
public void withCatch() {
try {
orderMapper.insert(new Order());
int x = 1 / 0;
} catch (Exception e) {
log.error("插入失败", e);
// 手动标记回滚,避免"吞异常=提交"
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
四、事务拦截器做了什么
代理.around:
TransactionInterceptor.invoke()
├─ 解析 @Transactional 属性(传播、隔离、rollbackFor、readOnly)
├─ 有事务?有 → 加入;没有 → 按传播行为开启
├─ 执行目标方法
├─ 正常返回 → commit(除非已 setRollbackOnly)
└─ 抛出异常 → 判断是否匹配 rollbackFor
匹配 → rollback
不匹配 → commit 后原样抛出(这就是受检异常不回滚的原因)
关键点:回滚判定看的是"拦截器捕获到的异常"。被 catch 掉、被包装成业务异常、被异步线程吞掉,都会让判定落空。
五、常见追问
- 问:为什么默认不回滚受检异常? 早期 EJB 的约定:受检异常代表"可恢复的业务结果",运行时异常代表"程序错误"。Spring 沿用了这个默认值,但允许
rollbackFor覆盖。 - 问:
@Transactional加在接口方法上有效吗? 有效但脆弱,CGLIB 代理下接口注解可能不被继承,建议标在实现类。 - 问:事务和
synchronized一起用要注意什么? 锁在方法内、事务在方法外(代理包住),可能出现"锁已释放、事务未提交"的窗口,导致并发读到旧值。应缩小事务范围或改用数据库锁。 - 问:
readOnly=true能防止写吗? 只是提示,MySQL 上会开启只读事务,但代码里仍可能写入;不能当权限校验。 - 问:多数据源下事务失效? 每个数据源需要各自的
PlatformTransactionManager,用@Transactional(transactionManager="xxxTxManager")指定,否则可能用错管理器导致"看似生效实际没事务"。
小结
排查顺序固定:代理有没有 → 方法能不能被拦 → 异常有没有传出去 → 存储支不支持。把 rollbackFor、吞异常、自调用三个高频坑讲清楚,再补"事务绑定线程"这一点,基本能覆盖面试与线上事故。