☰
@Transactional 加了却没生效?自调用、捕获异常、私有方法,四个经典翻车现场
2026/10/10 13:49:53 网站建设 项目流程

"我明明加了@Transactional,数据怎么还是写进去了?"这话我听了不下十遍,包括自己刚带团队时也翻过车。事务失效的场景翻来覆去就那么几个,今天把最常见的四种摆出来,对号入座。

先记住一个前提:Spring 事务靠 AOP 代理实现,只有方法走代理对象时才生效。凡是绕过代理的调用,注解都是摆设。

翻车现场一:同类内部自调用

最常见。同一个类里,方法 A 调方法 B,B 上标了@Transactional:

@ServicepublicclassOrderService{publicvoidcreateAndNotify(Orderorder){this.createOrder(order);// 自调用this.sendNotify(order);// 自调用}@Transactional(rollbackFor=Exception.class)publicvoidcreateOrder(Orderorder){orderMapper.insert(order);}}

createAndNotify调用createOrder走的是this,没经过代理,注解白挂。数据库照样插入,异常照样不回滚。

解决:注入自己(@Autowired private OrderService self)调self.createOrder(...);或者把事务方法挪到别的类/接口上;最省事的是用TransactionTemplate:

@ResourceprivateTransactionTemplatetransactionTemplate;publicvoidcreateAndNotify(Orderorder){transactionTemplate.execute(status->{orderMapper.insert(order);returnBoolean.TRUE;});}

翻车现场二:异常被 try-catch 吃了

事务方法内部把异常自己吞了,Spring 根本感知不到:

@Transactional(rollbackFor=Exception.class)publicvoiddeduct(Walletwallet,intamount){try{walletMapper.updateBalance(wallet,-amount);orderMapper.insert(order);// 这里炸了}catch(Exceptione){log.error("扣款失败",e);// 吞掉!}}

余额扣了、订单没插,因为异常没抛出去,事务照常提交。半成品数据就进去了。

两个方向:要么别在事务里 catch,要么 catch 后手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly():

}catch(Exceptione){log.error("扣款失败",e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();throwe;// 或者业务异常包装后继续抛}

我自己的习惯:事务方法里不写 try-catch,异常全部上抛,由最外层统一处理。

翻车现场三:private / 非 public 方法

注解标在private方法上,编译不报错,运行也不报错——就是不回滚。代理是基于接口或 CGLIB 的,private方法根本进不了代理链路:

@ServicepublicclassVipService{publicvoidupgrade(StringuserId){grantVip(userId);// 走代理,但 grantVip 是 private}@Transactional(rollbackFor=Exception.class)privatevoidgrantVip(StringuserId){vipMapper.upgrade(userId);integralMapper.add(userId,100);// 第二个失败,第一个不会回滚}}

把事务方法改成 public,或者把事务逻辑挪到一个独立 Service 里。private 上标注解属于典型的"看着对、其实废"。

翻车现场四:rollbackFor 没配,自定义异常不触发

@Transactional默认只回滚RuntimeException和Error。你自己定义的业务异常如果继承的是Exception,事务不回滚:

// 自定义检查异常publicclassBizExceptionextendsException{}@Transactional// 没写 rollbackForpublicvoidsettle(Orderorder)throwsBizException{settleMapper.update(order);if(!check(order)){thrownewBizException("校验不过");}}

BizException抛出去,前面的 update 照样提交。正确写法:

@Transactional(rollbackFor=Exception.class)publicvoidsettle(Orderorder)throwsBizException{...}

声明式不行就编程式:TransactionTemplate 保底

如果你发现自己总在跟"注解没生效"搏斗,干脆换个思路:编程式事务。TransactionTemplate不依赖代理,方法内部直接控制边界,自调用、私有方法这些坑全绕开:

@ResourceprivateTransactionTemplatetransactionTemplate;publicvoidbatchSettle(Listorders){transactionTemplate.executeWithoutResult(status->{for(Ordero:orders){settleMapper.update(o);integralMapper.add(o.getUserId(),o.getAmount());}});}

它的问题是样板代码多,每个方法都要包一层 execute。我的取舍:单方法小事务用注解,方法里逻辑重、要精确控制边界(比如部分提交、嵌套事务)时用编程式。两种都懂,别只会一种。

别把远程调用塞进事务里

别把远程调用塞进事务里

事务失效是回滚不了,还有一类问题是回滚过头:事务里调了远程接口(发短信、调支付、通知其他系统),事务回滚了,远程请求已经发出去了,两边数据不一致。我的铁律是:事务里只做数据库操作,远程调用一律放到事务提交之后:

@Transactional(rollbackFor=Exception.class)publicvoidsettle(Orderorder){settleMapper.update(order);integralMapper.add(order.getUserId(),order.getAmount());// 不在这里发通知!}publicvoidsettleAndNotify(Orderorder){settle(order);// 事务内notifyService.send(order);// 事务外,失败不影响主流程}

如果非要保证"数据库和通知都成功",正经做法是本地消息表 + 定时任务:事务里写一条notify_task记录,提交后定时任务扫表发送、失败重试。这套方案比"发完通知再提交"稳得多,也比"事务里直接调"干净得多。

顺带说个排查技巧:怀疑事务没生效,先在日志里看有没有Participating transaction或Creating new transaction字样。自调用时压根不会出现这两行日志——日志不会骗人,注解才会。

传播行为也是被误解的重灾区:内层方法标Propagation.REQUIRES_NEW想"独立回滚",前提是外层真开了事务;外层要是自调用(第一个坑),内层的 REQUIRES_NEW 挂在同一个代理链路上,行为跟你预期完全不一样。排查事务问题,永远先确认"这方法走代理了没",再谈传播行为——顺序反了,后面全是瞎猜。

踩坑复盘:零工系统结算重复入账

现象:日结系统里一笔订单的结算任务偶发重复入账,积分加了两次。排查过程:看日志发现OrderSettleService.settleAndNotify()里调了this.doPay(),而doPay()上标着@Transactional——标准的自调用失效。定位思路:结算任务先插入结算单、再加积分,两步在一个事务里;因为自调用绕过代理,第一步插入成功、第二步积分抛异常时,结算单也提交了,任务重试又把积分加了一遍。最终解决:把doPay拆到独立的PayService里,settleAndNotify通过注入的payService.doPay(...)调用;同时给结算单表加唯一索引(order_id + settle_type)做最后兜底。修完后重试场景再没出过双份。

可以直接抄走的清单

  • 自调用必失效:事务方法别用this.xxx()调,注入自己或拆独立 Service。
  • 异常别吞:事务方法里不 try-catch,或 catch 后setRollbackOnly()并继续抛。
  • private 是废的:事务方法必须 public,注解挂 private 上等于没挂。
  • rollbackFor 显式配:自定义异常继承Exception时必须rollbackFor = Exception.class。
  • 数据库兜底:关键写入加唯一索引,事务失效时还有最后一道防线。
  • 事务别写重逻辑:远程调用、消息发送放事务外,事务里只做库操作。

项目源码:https://gitee.com/gzqkl/qkl-boot

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询