☰
异常抛出后订单仍写进库:Spring Boot @Transactional 失效的 6 个复现点
2026/10/14 9:52:00 网站建设 项目流程

本文摘要:下单接口抛异常后订单仍写进库,注解在却找不到回滚依据。按异常生命周期拆出六条失效路径,逐条给出可运行复现与判定表。回滚要求可回滚异常穿过代理边界并绑定同一事务同步,异步多库另设边界。

一、现象与结论

POST /orders下单,orderService.case1Checked抛出CheckedBizException,HTTP 返回 500,控制台有完整堆栈;查orders表仍有order_no='B1'的行,id已生成。方法上有@Transactional,数据却留下了。

回滚生效要两个条件同时成立:异常以可回滚类型穿过方法代理边界,且所有写入绑定在同一事务同步上。六条失效路径破坏的正是这两件事——异常没到边界(吞掉、跨线程、提交后抛出)、类型不触发回滚(checked)、写入不参与同步(REQUIRES_NEW、异构通道)。

  1. checked异常未配rollbackFor:默认只回滚RuntimeException与Error。
  2. catch吞掉未重抛:拦截器只看到正常返回。
  3. 异常抛在另一个线程:事务上下文不跨线程。
  4. REQUIRES_NEW子事务先提交:主事务回滚带不走它。
  5. 写入通道不同步:另一DataSource或自动提交连接不受影响。
  6. 异常抛在提交之后:回滚窗口已关闭。

二、排查与判定

排查顺序:先确认异常是否到达代理边界,再看类型是否触发回滚,最后确认写入是否绑定同一事务同步。动手前先跑对照组baseline(RuntimeException,预期count = 0),排除连接池、表引擎等环境问题。

异常路径是否到达事务边界是否参与事务同步预期结果
checked异常到达,类型不触发回滚是提交留库
catch吞掉未到达是提交留库
跨线程抛出未到达是提交留库
REQUIRES_NEW主事务边界回滚子事务独立提交子事务留库
异构写入通道到达并回滚通道不同步通道留库
提交后抛出提交已完成是已落库

选择依据:单库单线程写入用注解加rollbackFor就够;出现异步、多数据源或分段提交,才需要编程式事务或补偿,取舍见第五节。

三、关键原理

代理模式下@Transactional由 AOP 代理在方法边界拦截,回滚判定发生在异常穿过代理边界的那一刻。默认规则是RuntimeException与Error回滚、checked异常提交,可用rollbackFor/noRollbackFor覆盖;注解方法需为public,且必须经代理对象外部调用才进入拦截器。

事务同步由PlatformTransactionManager绑定:JpaTransactionManager同步 JPAEntityManager,并可向同一DataSource的JdbcTemplate暴露该连接;手写DriverManager.getConnection或第二个DataSource不在同步内,回滚不带走它的写入。事务上下文不跨线程传递,提交完成后抛出的异常也不在事务生命周期内。

四、可运行示例

环境:Spring Boot 3.x(jakarta.*命名空间,最低 JDK 以官方 System Requirements 页为准),测试用 H2 内存库可跑复现点 1、2、3、4、6,点 5 需第二个DataSource或手写自动提交连接。在demo.order包放入下列类,执行mvn test。

// OrderService.javapackagedemo.order;importjava.util.concurrent.*;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;@ServicepublicclassOrderService{@AutowiredOrderRepositoryorders;@AutowiredInventoryServiceinventory;finalExecutorServiceworker=Executors.newSingleThreadExecutor();// 对照组:RuntimeException,预期回滚、count = 0@Transactionalpublicvoidbaseline(Stringno){orders.save(newOrderEntity(no,100L));thrownewIllegalStateException("风控拒绝");}// 复现点 1:checked 异常未配 rollbackFor,默认不回滚@Transactionalpublicvoidcase1Checked(Stringno)throwsCheckedBizException{orders.save(newOrderEntity(no,100L));thrownewCheckedBizException("风控拒绝");}// 复现点 2:异常被 catch 吞掉,方法正常结束即提交@Transactionalpublicvoidcase2Swallowed(Stringno){orders.save(newOrderEntity(no,100L));try{settle(no);}catch(RuntimeExceptione){System.err.println("结算失败:"+e.getMessage());// 未重抛}}// 复现点 3:异常抛在另一个线程,不经过本方法的代理边界@Transactionalpublicvoidcase3AnotherThread(Stringno){orders.save(newOrderEntity(no,100L));Runnabler=()->{thrownewIllegalStateException("异步风控拒绝");};try{worker.submit(r).get();}catch(InterruptedException|ExecutionExceptione){System.err.println("异步风控失败:"+e.getMessage());}}// 复现点 4:REQUIRES_NEW 子事务先提交,主事务回滚带不走它@Transactionalpublicvoidcase4RequiresNew(Stringno){inventory.deduct(no);orders.save(newOrderEntity(no,100L));thrownewIllegalStateException("主流程失败");}// 复现点 5:写入通道不在当前事务同步内@Transactionalpublicvoidcase5MixedChannel(Stringno,AuditWriteraudit){audit.insertAutoCommit(no);// 手写 Connection,自动提交orders.save(newOrderEntity(no,100L));thrownewIllegalStateException("失败");}// 复现点 6:方法正常返回即提交,异常抛在提交之后@Transactionalpublicvoidcase6AfterCommit(Stringno){orders.save(newOrderEntity(no,100L));}// 调用方 svc.case6AfterCommit(no); 之后再抛异常,行已落库privatevoidsettle(Stringno){thrownewIllegalStateException("结算通道超时");}}
// CheckedBizException.javapackagedemo.order;publicclassCheckedBizExceptionextendsException{publicCheckedBizException(Stringmsg){super(msg);}}// InventoryService.javapackagedemo.order;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Propagation;importorg.springframework.transaction.annotation.Transactional;@ServicepublicclassInventoryService{@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoiddeduct(Stringno){/* 扣库存写库,独立事务提交 */}}// AuditWriter.java:手写连接写审计表,不接入事务同步packagedemo.order;importjavax.sql.DataSource;importjava.sql.*;publicclassAuditWriter{privatefinalDataSourceds;publicAuditWriter(DataSourceds){this.ds=ds;}publicvoidinsertAutoCommit(Stringno){try(Connectionc=ds.getConnection();PreparedStatementp=c.prepareStatement("insert into audit_log(order_no) values(?)")){p.setString(1,no);p.executeUpdate();}catch(SQLExceptione){thrownewIllegalStateException(e);}}}// OrderEntity.java:@Entity,字段 orderNo、amount,@Id 自增(Boot 2.x 用 javax.persistence)// OrderRepository.java:public interface OrderRepository extends JpaRepository<OrderEntity, Long> {}
// OrderServiceRollbackTest.javapackagedemo.order;importorg.junit.jupiter.api.*;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importstaticorg.junit.jupiter.api.Assertions.*;@SpringBootTestclassOrderServiceRollbackTest{@AutowiredOrderServicesvc;@AutowiredOrderRepositoryorders;@BeforeEachvoidclean(){orders.deleteAll();}@Testvoidbaseline_rowGone(){assertThrows(IllegalStateException.class,()->svc.baseline("B0"));assertEquals(0L,orders.count());// 对照组:回滚链路是通的}@Testvoidcase1_checked_rowRemains(){assertThrows(CheckedBizException.class,()->svc.case1Checked("B1"));assertEquals(1L,orders.count());// 复现点 1}// case2~case6 同构:触发后断言 orders.count()}

预期输出:baseline用例count = 0,证明回滚链路本身是通的;case1用例count = 1,异常抛出但行已提交。

实际输出:未在目标版本实跑,以上断言值按声明式事务默认规则推断,标记为未验证;以mvn test的真实结果为准,若与推断不符,那正是最值得记录的版本差异。

观测配置:

logging:level:org.springframework.transaction:TRACEorg.springframework.jdbc.support:TRACE

五、失败处理与验证边界

加了rollbackFor = Exception.class仍不回滚:多为复现点 2、3,异常在到达代理边界前被吞掉或落在别的线程。改注解参数无效,应重抛RuntimeException,或用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()显式标记;REQUIRES_NEW的子事务留存属传播语义,不是失效。

测试全绿、生产不回滚:测试类/方法加了@Transactional时,Spring Test 的事务边界与生产不同,其默认提交/回滚行为以当前版本文档为准;一旦它默认回滚,count = 0只说明测试事务回滚了。应显式改变测试事务行为,或改用真实提交的集成测试再清数据。

多数据源半回滚:订单走 JPA、审计走手写Connection时,回滚后可能出现“订单没了、审计留着”。确认两路写入是否绑定同一事务管理器,必要时用@Transactional("beanName")指定;跨服务副作用只能靠本地消息表或 Saga 补偿。用 MySQL 时确认表引擎为InnoDB。

单库单线程纯 CRUD 不必引入编程式事务或补偿链路,注解加rollbackFor已够用。

替代方案与取舍

做法选择条件代价不该用的场景
rollbackFor = Exception.class业务异常是checked且确需回滚只修规则,不修吞异常与跨线程异常根本没到代理边界
业务异常继承RuntimeException异常契约可改的内部项目影响调用方签名与全局异常处理对外 SDK 已发布
TransactionTemplate编程式跨线程、异步、分段提交事务样板散布在业务代码单库单线程 CRUD
REQUIRES_NEW拆分子流程必须独立落库(库存、流水)短暂不一致,需要补偿要求严格原子性
唯一约束 / 幂等键跨服务、跨库副作用只约束单库期望应用层回滚兜底

参考资料

  • Spring Framework 参考文档:Declarative Transaction Management(默认回滚规则、方法可见性与代理说明)
  • Spring Framework API:@Transactional(rollbackFor/noRollbackFor属性表)
  • Spring Framework API:JpaTransactionManager(事务同步范围与连接暴露)
  • Spring Boot 参考文档:Testing(测试事务默认行为,以目标版本为准)

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

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

立即咨询