根本原因是事务传播机制与异常处理不匹配:内层异常使事务标记为rollback-only,外层吞异常后提交失败。解决方式包括:不捕获异常、改用REQUIRES_NEW或NESTED传播行为、或手动调用setRollbackOnly()。

这个问题本质是事务传播机制和异常处理逻辑不匹配造成的。外层方法开启事务,调用同样带 @Transactional 的内层方法,默认都用 REQUIRED 传播行为,实际共用同一个事务上下文。一旦内层抛出未捕获的运行时异常,Spring 就会把整个事务标记为 rollback-only;而外层如果用 try-catch 吞掉异常、又没做显式回滚,等到提交时就会爆这个错。
避免外层吞异常,让异常穿透事务边界
最直接的方式是不拦截内层异常,让它自然向上冒泡,由 Spring 的事务切面统一处理回滚:
- 删掉外层方法中对内层调用的
try-catch块 - 或保留
catch,但必须重新抛出(比如throw new RuntimeException(e)),确保事务管理器能感知到失败 - 注意:检查异常(如
IOException)默认不会触发回滚,若需回滚,得在@Transactional(rollbackFor = Exception.class)中显式声明
改用独立事务:REQUIRES_NEW
让内层方法运行在全新事务中,与外层事务完全隔离,互不影响:
- 给内层方法加上
@Transactional(propagation = Propagation.REQUIRES_NEW) - 这样即使内层失败回滚,外层事务仍可正常提交
- 适合“日志记录”“通知发送”等弱一致性操作,失败不影响主业务
- 注意:REQUIRES_NEW 会新开数据库连接,可能带来锁等待(比如外层已锁表,内层写同一张表时阻塞)
改用嵌套事务:NESTED
利用 JDBC Savepoint 实现真正的嵌套,内层异常只回滚到保存点,不污染外层事务:
立即学习“Java免费学习笔记(深入)”;
- 内层方法标注
@Transactional(propagation = Propagation.NESTED) - 要求底层数据库支持 Savepoint(MySQL、PostgreSQL、Oracle 都支持)
- 比 REQUIRES_NEW 更轻量,不新建连接,也不会因锁表导致阻塞
- 适合“部分补偿”场景,比如下单失败时只回滚库存扣减,不回滚用户积分更新
手动控制回滚状态
如果必须捕获异常且不能改传播行为,就主动告诉 Spring:“这个事务确实该回滚”:
- 在
catch块里调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() - 再决定是否继续抛出异常(通常建议抛出,便于上层感知)
- 这相当于绕过默认规则,显式接管回滚决策


















