事务失效的常见场景
引言
“我明明标了 @Transactional,数据怎么还是写进去了?”事务失效大多不是框架 bug,而是用法越过了代理/线程/传播的边界。本章把高频失效场景归成几类并给出排查顺序,一次讲透。
核心概念
- 代理边界类:
- 自调用(this 调用自身方法,绕过代理)——修复见第 13 章;
- 方法非 public(代理模式默认只拦 public,以官方文档为准);
- 注解标在接口/被 final 类方法上,且代理方式不支持。
- 异常处理类:
4. 方法内 try-catch 吞掉异常——事务不知道失败,自然提交;
5. 抛出的是受检异常又没配
rollbackFor——默认不回滚; 6. 回滚后异常被外层捕获继续执行“提交路径”。 - 传播理解类:
7. 内层 REQUIRED 方法异常被外层 catch——整个事务已被标记 rollback-only,最终提交时报
UnexpectedRollbackException; 8. 以为内层独立提交——需要 REQUIRES_NEW 才是独立事务。 - 资源/线程类: 9. 事务管理器与 DAO 数据源不一致——事务管不住 SQL; 10. 新开线程里调用 DAO——事务资源绑定当前线程(ThreadLocal),子线程不共享; 11. 数据库引擎不支持事务(如老 MyISAM)或自动提交被改动。
- 装配类:没开
@EnableTransactionManagement(Boot 自动开启的前提是存在事务管理器与对应 starter)、缺少PlatformTransactionManagerBean、多个事务管理器时没指定transactionManager名称。
代码示例
@Service
public class OrderService {
private final OrderDao orderDao;
public OrderService(OrderDao orderDao) { this.orderDao = orderDao; }
// 场景 4:异常被吞 → 不回滚
@Transactional
public void bad() {
try { orderDao.insert(order); throw new IllegalStateException("失败"); }
catch (RuntimeException e) { log.warn("忽略", e); } // ❌ 事务照常提交
}
// 场景 5:受检异常 → 默认不回滚
@Transactional
public void bad2() throws Exception { orderDao.insert(order); throw new Exception("x"); }
// 正确示范:不吞异常(或手动标记回滚),受检异常显式 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void good() throws Exception {
orderDao.insert(order);
throw new Exception("x"); // 现在会回滚
}
}
注意点
- 排查顺序建议:①调用是否穿过代理(自调用/非 public)→ ②异常是否到达事务边界(被吞/受检)→ ③传播与 rollback-only 语义 → ④数据源与事务管理器是否同一份 → ⑤线程是否一致。
- 日志看到
Transaction rolled back because it has been marked as rollback-only,去查“内层标了回滚、外层还在继续”的 REQUIRED 嵌套。 - 把事务边界尽量设计在“一个方法一次调用链”上,跨线程、跨数据源天然要另想方案(新事务/分布式事务)。
- 别用
@Transactional包住纯本地操作或长任务——事务会长时间占用数据库连接。
小结
失效根因就三类:没走代理(自调用/非 public)、异常没到边界(吞掉/受检)、事务资源不在同一上下文(线程/数据源/传播)。按上述顺序逐项排查,九成“事务不生效”立刻破案。