本文摘要:下单接口抛异常后订单仍写进库,注解在却找不到回滚依据。按异常生命周期拆出六条失效路径,逐条给出可运行复现与判定表。回滚要求可回滚异常穿过代理边界并绑定同一事务同步,异步多库另设边界。
一、现象与结论
POST /orders下单,orderService.case1Checked抛出CheckedBizException,HTTP 返回 500,控制台有完整堆栈;查orders表仍有order_no='B1'的行,id已生成。方法上有@Transactional,数据却留下了。
回滚生效要两个条件同时成立:异常以可回滚类型穿过方法代理边界,且所有写入绑定在同一事务同步上。六条失效路径破坏的正是这两件事——异常没到边界(吞掉、跨线程、提交后抛出)、类型不触发回滚(checked)、写入不参与同步(REQUIRES_NEW、异构通道)。
checked异常未配rollbackFor:默认只回滚RuntimeException与Error。catch吞掉未重抛:拦截器只看到正常返回。- 异常抛在另一个线程:事务上下文不跨线程。
REQUIRES_NEW子事务先提交:主事务回滚带不走它。- 写入通道不同步:另一
DataSource或自动提交连接不受影响。 - 异常抛在提交之后:回滚窗口已关闭。
二、排查与判定
排查顺序:先确认异常是否到达代理边界,再看类型是否触发回滚,最后确认写入是否绑定同一事务同步。动手前先跑对照组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(测试事务默认行为,以目标版本为准)