@Transactional 加了却没生效?自调用、捕获异常、私有方法,四个经典翻车现场

发布时间:2026/10/10 13:49:59
@Transactional 加了却没生效?自调用、捕获异常、私有方法,四个经典翻车现场
我明明加了Transactional数据怎么还是写进去了这话我听了不下十遍包括自己刚带团队时也翻过车。事务失效的场景翻来覆去就那么几个今天把最常见的四种摆出来对号入座。先记住一个前提Spring 事务靠 AOP 代理实现只有方法走代理对象时才生效。凡是绕过代理的调用注解都是摆设。翻车现场一同类内部自调用最常见。同一个类里方法 A 调方法 BB 上标了TransactionalServicepublicclassOrderService{publicvoidcreateAndNotify(Orderorder){this.createOrder(order);// 自调用this.sendNotify(order);// 自调用}Transactional(rollbackForException.class)publicvoidcreateOrder(Orderorder){orderMapper.insert(order);}}createAndNotify调用createOrder走的是this没经过代理注解白挂。数据库照样插入异常照样不回滚。解决注入自己Autowired private OrderService self调self.createOrder(...)或者把事务方法挪到别的类/接口上最省事的是用TransactionTemplateResourceprivateTransactionTemplatetransactionTemplate;publicvoidcreateAndNotify(Orderorder){transactionTemplate.execute(status-{orderMapper.insert(order);returnBoolean.TRUE;});}翻车现场二异常被 try-catch 吃了事务方法内部把异常自己吞了Spring 根本感知不到Transactional(rollbackForException.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(rollbackForException.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(rollbackForException.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(rollbackForException.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