复盘 Spring 事务失效的常见场景:自调用、回滚规则、访问修饰符与代理机制。
Spring 事务为什么会失效?一次 @Transactional 踩坑复盘h1
这篇来自一次真实翻车h2
我一开始学 Spring 事务时,感觉很简单:
- 方法上加
@Transactional - 出错就回滚
- 正常就提交
但真到写代码时,我遇到过一种很懵的情况:明明加了事务,数据还是提交了。
后来我才发现,很多“事务失效”并不是 Spring 不行,而是自己没有踩准它的生效条件。
结果有次我在本地压测时发现:明明接口抛错了,库里的数据还是落下去了。那次之后我才真正去啃“事务为什么会失效”。
这篇就是把那次排查里最关键的坑点整理出来。
我先把最容易踩的点列出来h2
@Transactional 不是加上就一定生效,常见失效场景有:
- 同类内部自调用
- 抛出的异常不在默认回滚范围内
- 方法不是
public - 事务方法没有经过 Spring 代理对象
所以重点不是“会不会写注解”,而是要知道:Spring 事务本质上依赖 AOP 代理。
一个最常见的场景h2
比如我有一个下单方法:
@Servicepublic 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
这是最经典的坑。
@Servicepublic class UserService {
public void register() { saveUser(); }
@Transactional public void saveUser() { // 保存用户 }}表面上看,register() 调用了带事务的 saveUser(),好像没问题。
但实际上,register() 是当前类内部直接调用,没有经过 Spring 生成的代理对象,所以事务不会生效。
为什么会这样h3
Spring 的事务是基于 AOP 代理实现的。只有外部通过代理对象调用事务方法时,Spring 才有机会:
- 开启事务
- 执行业务方法
- 决定提交还是回滚
而 this.saveUser() 这种调用,本质上是对象内部普通方法调用,绕过了代理。
常见解决方式h3
方式一:把事务方法拆到另一个 Serviceh4
@Servicepublic class UserService {
@Resource private UserTxService userTxService;
public void register() { userTxService.saveUser(); }}
@Servicepublic class UserTxService {
@Transactional public void saveUser() { // 保存用户 }}这种方式最清晰,也最常见。
方式二:通过代理对象调用自己h4
理论上也能做,但对学习阶段和普通业务代码来说,不如拆分 Service 更直观。
场景二:异常不回滚h2
Spring 默认只会对 运行时异常(RuntimeException) 和 Error 回滚。
比如下面这样:
@Transactionalpublic 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 后吃掉了异常。
@Transactionalpublic 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
很多时候我们会写成:
@Transactionalprivate void saveOrder() { // ...}或者:
@Transactionalprotected void saveOrder() { // ...}在常见 Spring 代理方式下,这类方法事务可能不会生效。学习阶段最稳妥的做法就是:
- 事务方法统一写成
public - 由外部 Bean 调用
场景五:自己 new 出来的对象没有事务h2
OrderService orderService = new OrderService();orderService.createOrder();这种方式也不会有事务,因为这个对象不是 Spring 容器管理的 Bean,没有代理。
所以事务有一个基础前提:对象要交给 Spring 管。
我现在的理解模型h2
我现在会把 Spring 事务理解成一句话:
@Transactional生效的关键,不只是注解本身,而是“这个方法有没有通过 Spring 代理对象被调用”。
如果这个前提不满足,后面的回滚规则都谈不上。
我现在排查事务问题的顺序h2
以后如果我发现“事务没生效”,我会按这个顺序排查:
- 这个类是不是 Spring Bean
- 这个事务方法是不是
public - 是不是外部通过代理调用,而不是内部自调用
- 抛出的是不是运行时异常
- 异常是不是被自己吃掉了
- 是否需要加
rollbackFor = Exception.class
我踩过的坑h2
坑 1:以为类里互相调用也算事务h3
其实不是。只要没经过代理,事务就进不来。
坑 2:看到报错就以为一定会回滚h3
不一定。异常类型不对,或者异常被吃掉,都可能不回滚。
坑 3:把事务写在细小 private 方法上h3
结果代码看起来很“优雅”,但事务根本没进来。
面试里我会这样讲这件事h2
可以按这个结构讲:
- 问题:业务方法加了
@Transactional,但异常后数据没有回滚 - 分析:发现是同类内部自调用,事务方法没有经过 Spring AOP 代理
- 方案:拆分事务方法到独立 Service,并明确回滚规则
- 收获:理解了 Spring 事务依赖代理机制,不再只停留在“会写注解”层面
最后总结h2
@Transactional 不是“加了就行”,而是“通过代理调用 + 满足回滚条件”才行。
评论