事务失效的常见场景

引言

“我明明标了 @Transactional,数据怎么还是写进去了?”事务失效大多不是框架 bug,而是用法越过了代理/线程/传播的边界。本章把高频失效场景归成几类并给出排查顺序,一次讲透。

核心概念

  • 代理边界类
    1. 自调用(this 调用自身方法,绕过代理)——修复见第 13 章;
    2. 方法非 public(代理模式默认只拦 public,以官方文档为准);
    3. 注解标在接口/被 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)、缺少 PlatformTransactionManager Bean、多个事务管理器时没指定 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)、异常没到边界(吞掉/受检)、事务资源不在同一上下文(线程/数据源/传播)。按上述顺序逐项排查,九成“事务不生效”立刻破案。

笔记加载中…