复盘 Spring 事务失效的常见场景:自调用、回滚规则、访问修饰符与代理机制。

Spring 事务为什么会失效?一次 @Transactional 踩坑复盘
/ Update
8 mins
1532 words
Loading views

Spring 事务为什么会失效?一次 @Transactional 踩坑复盘h1

这篇来自一次真实翻车h2

我一开始学 Spring 事务时,感觉很简单:

  • 方法上加 @Transactional
  • 出错就回滚
  • 正常就提交

但真到写代码时,我遇到过一种很懵的情况:明明加了事务,数据还是提交了。

后来我才发现,很多“事务失效”并不是 Spring 不行,而是自己没有踩准它的生效条件。

结果有次我在本地压测时发现:明明接口抛错了,库里的数据还是落下去了。那次之后我才真正去啃“事务为什么会失效”。

这篇就是把那次排查里最关键的坑点整理出来。

我先把最容易踩的点列出来h2

@Transactional 不是加上就一定生效,常见失效场景有:

  • 同类内部自调用
  • 抛出的异常不在默认回滚范围内
  • 方法不是 public
  • 事务方法没有经过 Spring 代理对象

所以重点不是“会不会写注解”,而是要知道:Spring 事务本质上依赖 AOP 代理。

一个最常见的场景h2

比如我有一个下单方法:

@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Transactional
public void createOrder() {
Order order = new Order();
order.setUserId(1001L);
order.setAmount(new BigDecimal("99.00"));
orderMapper.insert(order);
int x = 1 / 0;
}
}

如果这个方法通过 Spring 容器管理的 Bean 调用,那么抛出运行时异常后,事务会回滚。

但下面这些写法,就不一定了。

场景一:同类内部自调用h2

这是最经典的坑。

@Service
public class UserService {
public void register() {
saveUser();
}
@Transactional
public void saveUser() {
// 保存用户
}
}

表面上看,register() 调用了带事务的 saveUser(),好像没问题。

但实际上,register()当前类内部直接调用,没有经过 Spring 生成的代理对象,所以事务不会生效。

为什么会这样h3

Spring 的事务是基于 AOP 代理实现的。只有外部通过代理对象调用事务方法时,Spring 才有机会:

  1. 开启事务
  2. 执行业务方法
  3. 决定提交还是回滚

this.saveUser() 这种调用,本质上是对象内部普通方法调用,绕过了代理。

常见解决方式h3

方式一:把事务方法拆到另一个 Serviceh4

@Service
public class UserService {
@Resource
private UserTxService userTxService;
public void register() {
userTxService.saveUser();
}
}
@Service
public class UserTxService {
@Transactional
public void saveUser() {
// 保存用户
}
}

这种方式最清晰,也最常见。

方式二:通过代理对象调用自己h4

理论上也能做,但对学习阶段和普通业务代码来说,不如拆分 Service 更直观。

场景二:异常不回滚h2

Spring 默认只会对 运行时异常(RuntimeException)Error 回滚。

比如下面这样:

@Transactional
public void createOrder() throws Exception {
orderMapper.insert(order);
throw new Exception("下单失败");
}

很多人会以为这里也会回滚,但默认情况下不会,因为 Exception 属于受检异常

正确写法h3

@Transactional(rollbackFor = Exception.class)
public void createOrder() throws Exception {
orderMapper.insert(order);
throw new Exception("下单失败");
}

我的理解h3

如果你业务里可能抛自定义受检异常,最好明确写上:

@Transactional(rollbackFor = Exception.class)

这样更稳,也更容易和团队约定统一。

场景三:把异常吞掉了h2

还有一种情况也很常见:事务方法里明明报错了,但你自己 try-catch 后吃掉了异常。

@Transactional
public void createOrder() {
try {
orderMapper.insert(order);
int x = 1 / 0;
} catch (Exception e) {
log.error("下单异常", e);
}
}

这样事务通常也不会回滚,因为 Spring 看来,这个方法已经正常执行结束了。

更合理的处理方式h3

@Transactional(rollbackFor = Exception.class)
public void createOrder() {
try {
orderMapper.insert(order);
int x = 1 / 0;
} catch (Exception e) {
log.error("下单异常", e);
throw new RuntimeException(e);
}
}

核心点:如果你要让事务回滚,异常最终必须抛出去。

场景四:事务方法不是 publich2

很多时候我们会写成:

@Transactional
private void saveOrder() {
// ...
}

或者:

@Transactional
protected void saveOrder() {
// ...
}

在常见 Spring 代理方式下,这类方法事务可能不会生效。学习阶段最稳妥的做法就是:

  • 事务方法统一写成 public
  • 由外部 Bean 调用

场景五:自己 new 出来的对象没有事务h2

OrderService orderService = new OrderService();
orderService.createOrder();

这种方式也不会有事务,因为这个对象不是 Spring 容器管理的 Bean,没有代理。

所以事务有一个基础前提:对象要交给 Spring 管。

我现在的理解模型h2

我现在会把 Spring 事务理解成一句话:

@Transactional 生效的关键,不只是注解本身,而是“这个方法有没有通过 Spring 代理对象被调用”。

如果这个前提不满足,后面的回滚规则都谈不上。

我现在排查事务问题的顺序h2

以后如果我发现“事务没生效”,我会按这个顺序排查:

  1. 这个类是不是 Spring Bean
  2. 这个事务方法是不是 public
  3. 是不是外部通过代理调用,而不是内部自调用
  4. 抛出的是不是运行时异常
  5. 异常是不是被自己吃掉了
  6. 是否需要加 rollbackFor = Exception.class

我踩过的坑h2

坑 1:以为类里互相调用也算事务h3

其实不是。只要没经过代理,事务就进不来。

坑 2:看到报错就以为一定会回滚h3

不一定。异常类型不对,或者异常被吃掉,都可能不回滚。

坑 3:把事务写在细小 private 方法上h3

结果代码看起来很“优雅”,但事务根本没进来。

面试里我会这样讲这件事h2

可以按这个结构讲:

  • 问题:业务方法加了 @Transactional,但异常后数据没有回滚
  • 分析:发现是同类内部自调用,事务方法没有经过 Spring AOP 代理
  • 方案:拆分事务方法到独立 Service,并明确回滚规则
  • 收获:理解了 Spring 事务依赖代理机制,不再只停留在“会写注解”层面

最后总结h2

@Transactional 不是“加了就行”,而是“通过代理调用 + 满足回滚条件”才行。

评论