Spring 事务失效可能是哪些原因?

#Java#面试八股

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)
6Bean 不是 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、吞异常、自调用三个高频坑讲清楚,再补"事务绑定线程"这一点,基本能覆盖面试与线上事故。

笔记加载中…