Spring 事务失效的 8 种场景:@Transactional 加了却没生效怎么排查

「我明明加了 @Transactional,为什么异常抛出后数据没回滚?」这是 Spring 面试和线上事故里的高频问题。事务注解看起来简单,但它是靠 AOP 代理实现的,一旦绕过了代理,注解就成了摆设——不报错,悄悄失效,最后线上数据对不上账才发现。

这篇把最常踩的 8 种失效场景一次讲清,每种都给出「为什么失效」和「怎么修」,最后教你一招快速自查。

先理解一件事:@Transactional 靠代理生效

Spring 事务不是魔法,它的本质是:容器为你的 Bean 生成一个代理对象,在调用被 @Transactional 标注的方法前后,由代理帮你 begincommitrollback

调用方 → 代理对象(开事务/提交/回滚) → 真实对象的方法

只要调用没有经过这个代理,事务逻辑就被跳过了。 下面一大半失效场景,根子都在这句话上。

场景 1:方法不是 public

Spring 的事务代理只对 public 方法生效。protectedprivate、包级私有的方法即使加了注解也不会开启事务:

@Service
public class OrderService {
    @Transactional
    private void createOrder(Order o) { // private,事务不生效
        orderMapper.insert(o);
        throw new RuntimeException("boom"); // 不会回滚
    }
}

修法:改成 public。这是硬约束,基于 CGLIB/JDK 动态代理的机制决定的。

场景 2:自调用(同类内部方法互相调用)

这是最隐蔽、最高频的一种。同一个类里,方法 A 调用带 @Transactional 的方法 B:

@Service
public class UserService {

    public void register(User u) {
        // 直接 this.saveUser(u),走的是原始对象,不是代理对象
        saveUser(u); // 事务失效!
    }

    @Transactional
    public void saveUser(User u) {
        userMapper.insert(u);
        throw new RuntimeException("boom"); // 不会回滚
    }
}

register 里的 saveUser(u) 等价于 this.saveUser(u),this原始对象,不是 Spring 生成的代理对象,自然绕过了事务增强。

修法:让调用经过代理。三种方式:

// 方式一:注入自己(推荐,清晰)
@Service
public class UserService {
    @Autowired
    private UserService self; // 注入的是代理对象

    public void register(User u) {
        self.saveUser(u); // 走代理,事务生效
    }

    @Transactional
    public void saveUser(User u) { userMapper.insert(u); }
}
// 方式二:从容器里现取代理
((UserService) AopContext.currentProxy()).saveUser(u);
// 需要 @EnableAspectJAutoProxy(exposeProxy = true)

方式三是把 saveUser 拆到另一个 Bean 里,跨 Bean 调用天然走代理。生产上最推荐拆分到独立 Service,职责也更清晰。

场景 3:异常被 catch 吞了

事务回滚的触发条件是「方法抛出异常传播到代理层」。如果你在方法内部把异常 catch 住又没重新抛出,代理层根本感知不到异常:

@Transactional
public void saveUser(User u) {
    try {
        userMapper.insert(u);
        riskyCall(); // 抛异常
    } catch (Exception e) {
        log.error("出错了", e); // 吞掉了,代理以为一切正常 → 提交
    }
}

修法:要么别 catch 让它往上抛,要么在 catch 里手动标记回滚:

} catch (Exception e) {
    log.error("出错了", e);
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

场景 4:抛的是受检异常(checked exception)

Spring 默认只对 RuntimeExceptionError 回滚,对受检异常(Exception 的非运行时子类,如 IOException)不回滚:

@Transactional
public void save() throws IOException {
    userMapper.insert(u);
    throw new IOException("io failed"); // 默认不回滚,数据已提交
}

修法:显式指定回滚范围:

@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void save() throws IOException { ... }

团队里常把 rollbackFor = Exception.class 作为规范,避免踩这个默认值的坑。

场景 5:传播行为用错(propagation)

一个常见误解:方法 B 用 REQUIRES_NEW 开新事务,以为 B 失败不影响 A。但如果 A catch 了 B 抛出的异常继续走,B 的新事务确实回滚了,A 的事务却提交了,数据出现「一半成功」。反过来,默认的 REQUIRED 下 B 抛异常,即使 A catch 住,A 提交时也会收到 UnexpectedRollbackException,因为整个事务已被标记为 rollback-only。

修法:想清楚业务语义再选传播行为。「子操作失败不能影响主流程」用 REQUIRES_NEW 并妥善处理其异常;「要么全成要么全败」用默认 REQUIRED,且不要在中途 catch 后吞掉异常。

场景 6:数据库引擎不支持事务

代码全对,但底层表用的是 MyISAM 引擎——MyISAM 根本不支持事务,@Transactional 无从谈起:

-- 建表时不小心用了 MyISAM
CREATE TABLE orders (...) ENGINE=MyISAM;

修法:改成 InnoDB:

ALTER TABLE orders ENGINE=InnoDB;

新项目基本默认 InnoDB,但接手老库时值得 SHOW TABLE STATUS 确认一下。

场景 7:注解加在了接口或没被 Spring 管理的类上

如果 Bean 不是由 Spring 容器创建的(比如你自己 new 出来的),就没有代理,注解无效:

UserService s = new UserService(); // 自己 new 的,没有代理
s.saveUser(u); // 事务失效

修法:一定要通过 @Autowired / 构造注入从容器拿 Bean,不要手动 new。另外注解建议加在实现类的方法上,而不是接口方法上(CGLIB 代理下接口上的注解可能不被识别)。

场景 8:多线程调用

事务是绑定在当前线程的(通过 ThreadLocal 存事务上下文)。你在事务方法里另起一个线程执行 DB 操作,新线程拿不到这个事务:

@Transactional
public void batchSave(List<User> users) {
    users.forEach(u -> new Thread(() -> userMapper.insert(u)).start());
    // 子线程的插入不在当前事务里,主方法回滚也回滚不了它们
}

修法:事务方法内不要用新线程做需要一致性的 DB 写;确需并发,改用编程式事务在每个线程内各自管理,或重新设计为串行 + 批量。

一招快速自查

线上发现事务没回滚,按这个顺序排查最快:

  1. 方法是 public 吗?(场景 1)
  2. 是不是同类里 this. 自调用?(场景 2,最高频)
  3. 异常被 catch 吞了吗?(场景 3)
  4. 抛的是受检异常但没配 rollbackFor 吗?(场景 4)
  5. 表引擎是 InnoDB 吗?(场景 6)

前四条覆盖了 90% 的实际事故,尤其是自调用——看到 @Transactional 不生效,先怀疑它。

小结

  • @Transactional 靠 AOP 代理生效,任何绕过代理的调用都会让它失效,这是理解所有失效场景的总纲。
  • 最高频的两个坑:同类自调用(this. 绕过代理)、异常被 catch 吞掉。
  • 两个默认值要记牢:只对 public 方法生效、只对 RuntimeException 回滚(受检异常要配 rollbackFor = Exception.class)。
  • 基础设施层面:表必须是 InnoDB、Bean 必须由 Spring 管理、事务不跨线程。

一句话记忆:事务失效,九成是「调用没走代理」或「异常没传到代理」——盯住这两条,排查不会跑偏。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐