Spring 事务失效的 8 种场景:@Transactional 加了却没生效怎么排查
Spring 事务失效的 8 种场景:@Transactional 加了却没生效怎么排查
「我明明加了 @Transactional,为什么异常抛出后数据没回滚?」这是 Spring 面试和线上事故里的高频问题。事务注解看起来简单,但它是靠 AOP 代理实现的,一旦绕过了代理,注解就成了摆设——不报错,悄悄失效,最后线上数据对不上账才发现。
这篇把最常踩的 8 种失效场景一次讲清,每种都给出「为什么失效」和「怎么修」,最后教你一招快速自查。
先理解一件事:@Transactional 靠代理生效
Spring 事务不是魔法,它的本质是:容器为你的 Bean 生成一个代理对象,在调用被 @Transactional 标注的方法前后,由代理帮你 begin、commit 或 rollback。
调用方 → 代理对象(开事务/提交/回滚) → 真实对象的方法
只要调用没有经过这个代理,事务逻辑就被跳过了。 下面一大半失效场景,根子都在这句话上。
场景 1:方法不是 public
Spring 的事务代理只对 public 方法生效。protected、private、包级私有的方法即使加了注解也不会开启事务:
@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 默认只对 RuntimeException 和 Error 回滚,对受检异常(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 写;确需并发,改用编程式事务在每个线程内各自管理,或重新设计为串行 + 批量。
一招快速自查
线上发现事务没回滚,按这个顺序排查最快:
- 方法是
public吗?(场景 1) - 是不是同类里
this.自调用?(场景 2,最高频) - 异常被 catch 吞了吗?(场景 3)
- 抛的是受检异常但没配
rollbackFor吗?(场景 4) - 表引擎是 InnoDB 吗?(场景 6)
前四条覆盖了 90% 的实际事故,尤其是自调用——看到 @Transactional 不生效,先怀疑它。
小结
@Transactional靠 AOP 代理生效,任何绕过代理的调用都会让它失效,这是理解所有失效场景的总纲。- 最高频的两个坑:同类自调用(
this.绕过代理)、异常被 catch 吞掉。 - 两个默认值要记牢:只对
public方法生效、只对RuntimeException回滚(受检异常要配rollbackFor = Exception.class)。 - 基础设施层面:表必须是 InnoDB、Bean 必须由 Spring 管理、事务不跨线程。
一句话记忆:事务失效,九成是「调用没走代理」或「异常没传到代理」——盯住这两条,排查不会跑偏。
更多推荐



所有评论(0)