Spring Boot 事务失效:先定段落,再进分支
事务没起作用有两种形态:装配期直接抛异常,或者运行期静默不回滚。先读报错签名和事务 DEBUG 日志,判断自己卡在「事务管理器装配、代理边界、传播行为、回滚判定」哪一段,再按分支里的判据命令逐条排除成因。

按首个可操作的异常分流
事务要真正生效得过四关:容器里有一个可用的事务管理器、AOP 代理把方法包住了、传播行为拿到了正确的物理连接、异常按规则触发回滚。任何一关没过,现象都是「事务没起作用」。先分清你遇到的是装配期的硬报错,还是运行期的静默失效,再按报错签名或日志对号进分支。
当前环境
先记录持久层用的是 JDBC/MyBatis 还是 JPA、启动类有没有 exclude 过自动配置、项目里是否自定义了 DataSource Bean。这三件事决定了事务管理器该由谁注册。
- 确认依赖里有没有 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa
- 确认 @SpringBootApplication 的 exclude / excludeName 列表
- 确认是否有多数据源或公共 DataConfig 之类自定义了 DataSource Bean
报错识别
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No bean named 'transactionManager' available:
expected at least 1 bean which qualifies as autowire candidate.
No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available:
expected at least 1 bean which qualifies as autowire candidateNo bean named 'transactionManager' available→ 按默认名字找不到事务管理器,命中本分支;走下方三条判据定因异常在启动时就抛出→ 不是这个分支——装配期报错通常是 DataSource 配置失败,属于数据源链路expected single matching bean but found 2→ 不是缺失而是二义性,去 A2
爆发点在第一个事务方法调用时,不在启动阶段;异常栈里一定有 TransactionAspectSupport.determineTransactionManager。
快速解法
三条判据分别对应三种成因:自动装配被排除、缺依赖、自定义数据源后没补管理器。按序执行,用输出锁定成因。
判据一:查是否排除了事务自动配置。有输出 → 自动装配被 exclude,删掉 exclude 即可
grep -rn "exclude.*TransactionManager\|excludeName.*TransactionManager" src/main/java判据二:看自动配置报告。DataSourceTransactionManagerAutoConfiguration 落在 NegativeMatches → 缺 spring-boot-starter-jdbc/data-jpa
java -jar app.jar --debug | grep -A3 "DataSourceTransactionManagerAutoConfiguration"判据三:查自定义 DataSource Bean。自定义了 → 自动配置整体退让,必须手动补事务管理器
grep -rn "@Bean" -A2 src/main/java | grep -n "DataSource"修复(自定义数据源或多数据源场景):手动注册管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}验证
- 启动后 /actuator/beans 里能看到 PlatformTransactionManager,或日志中自动配置报告显示匹配成功
- 调用事务方法不再抛 NoSuchBeanDefinitionException
- 管理器名字不叫 transactionManager 时,在 @Transactional(transactionManager = "...") 显式指定
解法无效时
- 多数据源场景:每个库一套 DataSource + TransactionManager,逐个确认是否都注册齐了
- JPA 项目要确认管理器类型是 JpaTransactionManager;错用 DataSourceTransactionManager 会导致 EntityManager 不参与事务
相关
- 关联案例:No bean named 'transactionManager' 事务管理器缺失排查
- A2:管理器存在但有多个候选,报的是 NoUniqueBeanDefinitionException
- 官方文档:https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.transaction
当前环境
记录下来是单数据源还是多数据源、容器里注册了几个 PlatformTransactionManager、哪些 @Transactional 没写 transactionManager。二义性几乎都是在单库改多库时引出来的。
- 列出所有 PlatformTransactionManager Bean 的名字,标出哪个是主库默认
- 统计没有显式指定事务管理器的 @Transactional 调用点
报错识别
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available:
expected single matching bean but found 2: orderTransactionManager, userTransactionManager
found 2: xxxTransactionManager, yyyTransactionManager→ 容器里有多个等价候选,命中本分支;缺 @Primary 或未显式指定found 1,但仍然报 NoSuchBean→ 名字对不上(例如叫 txManager),实际是 A1,按 A1 处理启动正常、第一个事务方法调用才炸→ 与 A1 同一爆发点,区别只在 found N 这句,别混为一谈
同上,错误不在启动期爆发。
快速解法
两条路:给默认库标 @Primary 快速恢复存量代码,或逐个方法显式绑定。前者止血,后者才是终态。
判据:列出所有事务管理器定义,确认名字与默认库
grep -rn "PlatformTransactionManager" src/main/java修法一(止血):给默认库管理器加 @Primary
@Bean
@Primary
public PlatformTransactionManager orderTransactionManager(@Qualifier("orderDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}修法二(推荐):每个事务方法显式绑定,让语义与物理库一致
@Transactional(transactionManager = "orderTransactionManager")
public void createOrder(Order o) { orderMapper.insert(o); }验证
- 事务方法不再抛 NoUniqueBeanDefinitionException
- 开事务 DEBUG 确认 Creating new transaction 里管理的连接与目标库一致
- 跨库写入的方法逐个核对绑定的事务管理器
解法无效时
- 读写分离场景:@Transactional(readOnly = true) 也要绑到从库管理器,否则只读事务开在主库上
- 遗留代码量大时先在 @Primary 下恢复运行,再按业务域逐步补全显式绑定
相关
- 关联案例:多数据源 NoUniqueBeanDefinitionException 二义性排查
- A1:容器里一个管理器都没有,报的是 No bean named 'transactionManager' available
- 官方文档:https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html#tx-multiple-tx-mgrs-with-attransactional
当前环境
记录事务方法的调用方式:是外部 Bean 通过注入的引用调的,还是同一个类里用 this 调的。这一步最常被跳过,但它是这类问题的唯一分水岭。
- 确认调用点是 this.xxx() 还是注入进来的 Bean
- 确认 @Async / @Transactional / @Cacheable 是不是都从同一个入口进的
- 确认方法本身是 public
报错识别
logging.level.org.springframework.transaction.interceptor=DEBUG 打开后:
正常事务调用 → Creating new transaction with name [...]
自调用场景 → 整条调用链一句事务日志都没有"http-nio-8080-exec-1" #30 daemon prio=5 os_prio=0
java.lang.Thread.State: RUNNABLE
DEBUG 下没有 Creating new transaction,且调用点是 this.xxx()→ 走的是原始对象,拦截器没参与,命中本分支有 Creating new transaction,数据却没回滚→ 事务开起来了,问题在回滚规则,去 04 阶段方法被 final 修饰→ 同样是拦截器没织入,但成因不同,去 B2
同一根因也适用于 @Async / @Cacheable:凡是靠 AOP 代理生效的注解,this 自调用一律失效。
快速解法
先用日志坐实「事务压根没开」,再按判据二确认调用写法,最后选修法。
判据一:开事务 DEBUG。正常事务有 Creating new transaction,自调用时整条链路一句都没有
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.transaction.interceptor: DEBUG判据二:检索 this 直调事务方法的调用点
grep -rn "this\.[a-zA-Z]*(" src/main/java --include="*Service*.java"修法(推荐):把事务方法拆到独立 Bean,通过注入调用,天然走代理
@Service
public class OrderService {
private final AsyncEmailService asyncEmailService;
public void process(Long orderId) {
asyncEmailService.sendEmail(orderId);
}
}修法(快速止血):注入自身代理,@Lazy 避免构造期循环依赖
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self;
public void handle(Order order) {
self.sendNotification(order.getId());
}
}验证
- 事务 DEBUG 里出现 Creating new transaction
- 在事务方法中构造异常,确认前面的写入确实回滚
解法无效时
- @Async 方法里打印 Thread.currentThread().getName(),还是 http-nio-* 就说明没进线程池
- 拆 Bean 后仍无事务日志 → 查 B2(final)与 B3(可见性);@Async 子线程拿不到事务 → 见 C3
相关
- 关联案例:@Async/@Transactional 同类调用 self-invocation 失效
- B2:final 类/方法导致 CGLIB 建不出代理,现象和自调用一模一样
- 本质:Java 方法体内的 this 永远指向原始实例,不会重新回到代理
- 官方文档:https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
当前环境
记录 Spring Boot 版本与代理模式。Boot 2.0 起 spring.aop.proxy-target-class 默认为 true,所有 @Transactional Bean 都走 CGLIB 代理,而 CGLIB 靠继承目标类生成子类——final 正好卡在这条路上。
- 检查事务类和方法上有没有 final 修饰
- 确认类是否实现了接口(决定能不能退回 JDK 动态代理)
报错识别
org.springframework.aop.framework.AopConfigException:
Could not generate CGLIB subclass of class com.example.OrderService:
Common causes of this problem include using a final class or a non-visible class
@Service
public class OrderService {
@Transactional
public final void createOrder(Order o) { orderMapper.insert(o); }
}
// 启动正常、调用正常返回,DEBUG 下没有 Creating new transactionAopConfigException ... Could not generate CGLIB subclass→ 类是 final,代理子类建不出来,启动就炸,命中本分支启动正常但 DEBUG 无 Creating new transaction→ 方法 final 或同类自调用,两者现象一致,先查 final 修饰类是 final 但跑得好好的→ 可能走了接口代理,不过仍建议去掉 final,避免换实现时踩坑
AopUtils.isAopProxy(this) 返回 true 只说明类被代理了,不代表每个方法都织入了拦截器,final 方法的失效就藏在这个缝里。
快速解法
先定位 final 在哪一层,再选改法。类 final 是硬报错好找,方法 final 是静默失效要靠判据二抓。
判据一:检索 final 类与 final 方法
grep -rn "public final class\|final void\|public final .*(.*)" src/main/java --include="*Service*.java"判据二:运行时确认代理类型与织入情况
System.out.println(AopUtils.isAopProxy(this));
System.out.println(AopUtils.isCglibProxy(this));修法(推荐):去掉事务类/事务方法上的 final,final 字段不用动
@Transactional
public void createOrder(Order o) { orderMapper.insert(o); }兜底:确实去不掉 final 时,用编程式事务绕开注解代理
transactionTemplate.executeWithoutResult(status -> orderMapper.insert(o));验证
- 启动不再抛 AopConfigException
- 事务 DEBUG 出现 Creating new transaction,异常时数据回滚
解法无效时
- 去 final 后仍无事务日志 → 查 B1(自调用)与 B3(可见性)
- 团队规范里禁止给 Spring Bean 的业务方法加 final,review 阶段就能拦住
相关
- 关联案例:final 类/方法导致 CGLIB 无法代理、事务失效
- B1:自调用的现象与 final 方法一模一样,先看调用方式再看修饰符
- 官方文档:https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
当前环境
记录方法可见性和代理类型。private 方法任何代理都拦不到;protected / 包级方法在 CGLIB 代理下从 Spring 6.0 起可以生效,但接口代理(JDK 动态代理)下的方法必须是 public 且在接口里声明。
- 确认注解方法的可见性:private、protected、包级还是 public
- 确认代理类型:类是 CGLIB(目标类代理)还是 JDK 动态代理(接口代理)
- 确认调用方是外部 Bean 还是 this 自调用
报错识别
@Service
public class OrderService {
public void createOrder(Order o) {
doCreate(o); // 外部进来是代理,但下一步没走代理
}
@Transactional // private:注解编译得过、运行被忽略
private void doCreate(Order o) { orderMapper.insert(o); }
}
// 无异常、无事务日志,数据不回滚grep -rn -B3 "@Transactional" src/main/java --include="*Service*.java" | grep -E "private|protected"方法是 private→ CGLIB 与 JDK 代理都无法拦截 private 方法,注解静默失效,命中本分支protected / 包级 + CGLIB 目标类代理 + Spring 6.0+→ 这类方法是能生效的,不是这个分支,去 B1 查调用方式接口代理(JDK 动态代理)下的非 public 方法→ 接口代理只能拦接口里声明的 public 方法,命中本分支方法 public 但同类内部调用→ 可见性没问题,是代理边界问题,去 B1
官方明确:代理模式下只有穿过代理的外部方法调用来会被拦截;private 方法不在可拦截范围内,Spring 不会为此报错。
快速解法
先确认可见性判据,再把事务边界上移到代理能拦到的位置,而不是硬把 private 改成 public。
判据一:检索非 public 方法上的 @Transactional
grep -rn -B3 "@Transactional" src/main/java --include="*Service*.java" | grep -E "private|protected"判据二:确认调用方走的是代理还是 this,可用 AopUtils 辅助判断
System.out.println(AopUtils.isAopProxy(this));修法(推荐):事务边界上移到 public 方法,private 方法只保留纯逻辑
@Transactional
public void createOrder(Order o) {
doCreate(o);
}
private void doCreate(Order o) { orderMapper.insert(o); }备选:private 逻辑确实需要独立事务时,抽到独立 Bean 的 public 方法上
@Service
public class OrderTxService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(Order o) { orderMapper.insert(o); }
}验证
- 事务 DEBUG 里出现 Creating new transaction
- 在事务方法中构造异常,确认数据回滚
- 接口代理场景确认方法已声明在接口里且为 public
解法无效时
- 改成 public 后仍无事务日志 → 回到 B1(调用方式)与 B2(final)
- 团队希望代理行为一致时,可显式开启 publicMethodsOnly,让非 public 方法被一致忽略,避免「看起来能用」的错觉
相关
- 关联案例:@Transactional 标在 private 方法上失效
- B1:public 方法同类自调用同样绕过代理,两条分支常一起出现
- 官方文档:https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html
当前环境
记录哪些方法标了 REQUIRES_NEW、它们写入的数据是否与主业务强相关。REQUIRES_NEW 的独立提交是双向的:外层回滚救不了已提交的内层,内层也看不到外层未提交的数据。
- 列出所有 Propagation.REQUIRES_NEW 声明点
- 逐个判断:外层回滚时,这份数据单独留着是否合法
- 确认连接池容量,REQUIRES_NEW 会多占一条连接
报错识别
Creating new transaction with name [OrderService.createOrder]
Suspending current transaction, creating new transaction with name [AuditService.record]
Initiating transaction commit // 内层立即真实提交
Resuming suspended transaction after completion of inner transaction
Initiating transaction rollback // 外层回滚,已提交的内层纹丝不动Suspending current transaction, creating new transaction→ 事务被切成两段不同物理连接,内层独立提交,命中本分支Participating in existing transaction→ 内层加入了外层事务,共用连接,不是这个分支,去 D3内层数据残留、外层数据回滚→ 这正是 REQUIRES_NEW 的语义,不是框架 bug,是选型问题
检索方式:grep -rn "REQUIRES_NEW" src/main/java。
快速解法
先用日志确认是挂起而非加入,再按业务一致性要求决定改回 REQUIRED 还是保留独立事务。
判据一:列出所有 REQUIRES_NEW 声明,逐个追问外层回滚时数据独自留存是否合法
grep -rn "REQUIRES_NEW" src/main/java判据二:开事务管理器 DEBUG,看日志是 Suspending 还是 Participating
logging:
level:
org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG修法(强一致场景):改回默认 REQUIRED,与外层同生共死
@Transactional(propagation = Propagation.REQUIRED)
public void record(Order order, String action) {
auditMapper.insert(new AuditLog(order.getId(), action));
}修法(确需独立留痕):保留 REQUIRES_NEW,但只写无外键强约束的旁路记录
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordAccessLog(String traceId, String action, String payload) {
auditMapper.insert(traceId, action, payload);
}验证
- 改用 REQUIRED 后,外层异常时内层数据同步回滚
- 日志里不再出现 Suspending current transaction
- 保留 REQUIRES_NEW 的旁路记录,确认不依赖主业务主键
解法无效时
- 审计、防重、操作留痕这类确实要求「主流程失败也要留」的需求,保留 REQUIRES_NEW 是合理的
- 连接池打满时排查挂起事务数量,REQUIRES_NEW 是常见的连接占用放大器
相关
- 关联案例:REQUIRES_NEW 内层提前提交产生脏数据
- C2:NESTED 靠保存点做局部回滚,和 REQUIRES_NEW 的独立提交是两套机制
- D3:内层异常标记 rollback-only 后外层还想提交,报的是 UnexpectedRollbackException
当前环境
记录事务管理器是 JpaTransactionManager 还是 DataSourceTransactionManager、持久层是 JPA/Hibernate 还是 MyBatis/JDBC。NESTED 依赖数据库保存点,而 JPA 方言默认不提供。
- 确认事务管理器实现类
- 列出所有 Propagation.NESTED 使用点
- 确认是从 MyBatis/JDBC 迁到 JPA 的项目(迁移是这类问题的高发场景)
报错识别
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction;
nested exception is javax.persistence.PersistenceException:
org.hibernate.TransactionException: JpaDialect does not support savepoints:
org.springframework.orm.jpa.vendor.HibernateJpaDialect
JpaDialect does not support savepoints→ JPA 方言不支持保存点,NESTED 无法实现,命中本分支NestedTransactionNotSupportedException→ 同类问题的另一种表述:当前管理器不支持嵌套事务NESTED 在纯 JDBC / MyBatis 下跑得好好的→ DataSourceTransactionManager 原生支持保存点,这不是 JPA 环境,去查别的分支
JPA 规范里没有 JDBC Savepoint 的标准化概念,HibernateJpaDialect.supportsSavepoints() 直接返回 false。
快速解法
先确认管理器类型坐实成因,再按业务一致性要求选替换方案。
判据一:确认事务管理器类型,是 JpaTransactionManager 就会命中
grep -rn "JpaTransactionManager\|DataSourceTransactionManager" src/main/java判据二:列出所有 NESTED 使用点
grep -rn "Propagation.NESTED" src/main/java修法(业务允许整体回滚):去掉 NESTED 与 try-catch,改回 REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void giftPoints(Long userId, int point) {
pointRecordRepository.save(new PointRecord(userId, point));
}修法(子操作失败不影响外层):改用 REQUIRES_NEW,注意独立提交语义(见 C1)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void giftPoints(Long userId, int point) {
pointRecordRepository.save(new PointRecord(userId, point));
}验证
- 不再抛 TransactionSystemException / NestedTransactionNotSupportedException
- 构造子操作失败,确认数据状态符合业务预期(整体回滚或仅子操作独立)
解法无效时
- 从 MyBatis/JDBC 迁到 JPA 的项目,全局搜 NESTED 统一整改
- 确实依赖 savepoint 做高频局部回滚时,只能保留纯 JDBC 事务管理器,混用 JPA 与 DataSourceTransactionManager 会让 EntityManager 脱离事务
相关
- 关联案例:NESTED 传播在 JPA 下报 TransactionSystemException
- C1:REQUIRES_NEW 的独立提交,常被当成 NESTED 的替代品误用
- 官方文档:https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html
当前环境
记录事务方法里有没有 new Thread、@Async 或自定义线程池提交。事务上下文用 ThreadLocal 存在当前线程上,事务跟着线程走,不跟着方法调用链走。
- 检索事务方法内的 new Thread / @Async / executor.submit
- 确认这些子任务里的写操作是否被期望随外层回滚
- 确认线程池是否配了 TaskDecorator 之类的上下文传递
报错识别
Creating new transaction with name [com.example.OrderService.createOrder] // 主线程
Creating new transaction with name [null] // 子线程:不是 Participating,说明没加入外层
Initiating transaction commit // 子线程先提交
Initiating transaction rollback // 主线程回滚,子线程数据纹丝不动子线程日志是 Creating new transaction 而非 Participating→ 事务没传播到子线程,子线程各开各的,命中本分支子线程查不到外层刚插入未提交的数据→ 同一根因:两段事务在不同的物理连接上,互相不可见外层回滚后子线程数据仍在→ 典型的静默脏数据,不是框架 bug 而是使用方式问题
TaskDecorator / MDC 传递的是日志上下文,不包含事务上下文,别指望它把事务带过去。
快速解法
先用日志坐实子线程没加入外层事务,再按「是否需要同生共死」选修法。
判据一:开事务管理器 DEBUG,比对主线程与子线程的事务日志
logging:
level:
org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG判据二:检索事务方法内的多线程调用点
grep -rn "new Thread\|@Async\|\.submit(" src/main/java --include="*Service*.java"修法(需要同生共死):写操作留在主线程同一事务里,不要丢进子线程
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 需要原子性的写操作就在本线程完成,别丢给 new Thread
}修法(确需异步):主事务提交后再触发异步,用事务同步回调保证顺序
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
public void afterCommit() {
asyncService.send(order.getId());
}
});验证
- 子线程不再出现 Creating new transaction,或改成 afterCommit 回调后由主事务提交触发
- 构造外层异常,确认不再有子线程写入的数据残留
解法无效时
- 子线程必须独立成功时,接受它单独开事务与单独提交,并在业务上保证幂等与补偿
- 分布式场景的跨服务原子性属于另一条链路,本地 @Transactional 无能为力
相关
- 关联案例:事务方法内开新线程事务不传播
- B1:@Async 方法被同类自调用时连线程池都进不去,是另一个层次的失效
- 官方文档:https://docs.spring.io/spring-framework/reference/data-access/transaction/tx-resource-synchronization.html
当前环境
记录抛出的异常类型和 @Transactional 有没有写 rollbackFor。Spring 默认只对 RuntimeException 和 Error 回滚,受检异常被视作预期内分支,照常提交。
- 确认抛出的异常继承自 Exception 还是 RuntimeException
- 确认自定义业务异常的基类
- 统计没有显式 rollbackFor 的 @Transactional
报错识别
Creating new transaction with name [com.example.service.ImportService.importData]
Completing transaction for [com.example.service.ImportService.importData] after throwing: java.io.IOException: file corrupted
Initiating transaction commit // 抛了异常却提交,说明命中了不回滚的类型
Committing JDBC transaction on Connection [HikariProxyConnection@12345678]after throwing 之后接 Initiating transaction commit→ 异常类型不满足回滚规则,命中本分支抛出的是 RuntimeException / Error→ 默认就会回滚,不是这个分支,去 D2 查异常是否被吞自定义异常继承自 Exception→ 同样按受检异常处理、默认不回滚,属于本分支
DefaultTransactionAttribute.rollbackOn 只认 RuntimeException 和 Error,这是沿袭 EJB 时代的约定。
快速解法
先用判据确认异常谱系,再决定是逐个补 rollbackFor 还是从架构层统一。
判据一:检索继承自 Exception 的自定义异常
grep -rn "class .* extends Exception" src/main/java --include="*Exception*.java"判据二:统计没有显式 rollbackFor 的 @Transactional
grep -rn "@Transactional" src/main/java | grep -v "rollbackFor"修法(推荐):统一加 rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
public void importData(File file) throws IOException {
batchMapper.insertAll(parseData(file));
}修法(架构层):业务异常基类统一继承 RuntimeException
public class BaseBusinessException extends RuntimeException {
private final String errorCode;
public BaseBusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}验证
- 日志里 after throwing 之后变成 Initiating transaction rollback
- 构造一个受检异常,确认前置写入一起回滚
解法无效时
- 加了 rollbackFor 仍提交 → 查 D2(被内层 catch)与 D3(内层已标记 rollback-only)
- 精准控制回滚范围时可用 rollbackFor = {IOException.class, SQLException.class} 指定具体类型
相关
- 关联案例:受检异常不回滚 rollbackFor 默认规则的坑
- D2:异常被方法内 catch 掉,连回滚规则都触发不了
- 官方文档:https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html
当前环境
记录 @Transactional 方法内的 catch 块有没有重新抛出异常。回滚的前提是异常穿过代理边界,方法内吞掉异常对代理来说就是一次正常返回。
- 列出事务方法内只打日志不重抛的 catch 块
- 确认这些 catch 是想降级返回,还是单纯忘了抛
- 确认子操作是核心链路还是可容忍失败的旁路
报错识别
2026-09-14 10:02:11 ERROR c.e.OrderService : 库存扣减失败: insufficient stock
Completing transaction for [OrderService.createOrder] after return: null
// ^^^^^^^^^^^ 正常返回,走提交流程Completing transaction ... after return→ 代理收到的是正常返回,走提交,命中本分支after throwing: xxxException→ 异常到了代理层,去 D1 看回滚规则是否匹配业务日志有 ERROR、事务却提交了→ 「报错了却没坏」的典型表现,本质是内部 catch 对代理报喜不报忧
排查命令:grep -rn -A2 "catch.*Exception" src/main/java --include="*Service*" | grep -B1 "log.error"。
快速解法
先用日志确认是 after return,再用判据二定位吞异常的 catch,最后按「要不要保留降级」选修法。
判据一:开事务拦截器 DEBUG,看是 after return 还是 after throwing
logging:
level:
org.springframework.transaction.interceptor: DEBUG判据二:检索事务方法内只打日志不重抛的 catch
grep -rn -A2 "catch.*Exception" src/main/java --include="*Service*" | grep -B1 "log.error"修法(推荐):catch 后重新抛出,让代理感知异常
try {
inventoryService.deduct(order.getSkuId(), order.getCount());
} catch (BusinessException e) {
log.error("库存扣减失败: {}", e.getMessage());
throw new RuntimeException("库存扣减失败,触发回滚", e);
}修法(不能中断响应时):catch 内手动标记回滚
try {
inventoryService.deduct(order.getSkuId(), order.getCount());
} catch (BusinessException e) {
log.error("库存扣减失败,标记事务回滚: {}", e.getMessage(), e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}验证
- 日志里变成 after throwing,事务进入回滚分支
- 构造子调用失败,确认前置写入一并回滚
解法无效时
- 想保留降级返回又不想整体回滚 → 子操作抽独立 Bean + REQUIRES_NEW,再在外层处理失败
- catch 只做日志上报、不做业务补偿时,直接删掉 catch 更清晰
相关
- 关联案例:try-catch 吞掉异常事务照常提交
- D3:内层已标记 rollback-only 而外层仍想提交,报 UnexpectedRollbackException
- C1:真正想让子操作独立于主流程时,用 REQUIRES_NEW 而不是吞异常
当前环境
记录内外层的传播行为和异常处理方式。默认 REQUIRED 下内外层共用同一条物理连接,内层一旦触发回滚条件就把整个物理事务标记成只能回滚。
- 确认内外层分别用了什么传播行为
- 确认外层是否 catch 了内层异常并继续走后续逻辑
- 确认日志里 rollback-only 标记出现在哪一帧
报错识别
org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only
Participating in existing transaction
// 内层抛出业务异常 → 共享事务被标记 rollback-only
Initiating transaction commit
// 提交阶段校验到标记 → 强制回滚并抛 UnexpectedRollbackExceptionUnexpectedRollbackException ... marked as rollback-only→ 内层已把共享事务判了死刑、外层还想提交,命中本分支异常栈里只有 UnexpectedRollbackException,没有业务异常→ 它是哨兵信号,真正根因在被标记 rollback-only 的前一帧,往上翻日志内层用的是 REQUIRES_NEW→ 独立事务不共享 rollback-only 标记,不是这个分支,去 C1
这个异常几乎从不指向自身的问题,别在它上面加 try-catch 了事。
快速解法
先顺着 rollback-only 找到把它标上去的内层方法,再决定是「整体回滚」还是「内层改独立事务」。
判据一:在日志里搜 rollback-only 出现的位置,定位到内层方法
grep -n "rollback-only" logs/app.log判据二:确认内层的传播行为与异常类型
grep -rn "Propagation\." src/main/java --include="*Service*.java"修法(推荐):内层异常继续抛出,让整体一起回滚,不要在中间层吞掉
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
pointService.gift(order.getUserId(), 100); // 抛出去,别 catch
}修法(保留容错降级):子操作抽独立 Bean,声明 REQUIRES_NEW
@Service
public class PointService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void gift(Long userId, int point) { pointRecordMapper.insert(userId, point); }
}验证
- 不再出现 UnexpectedRollbackException
- 主业务正常时数据完整提交,异常时整体回滚,不存在部分提交
解法无效时
- 嵌套多层事务时逐层核对接线与 rollback-only 标记的来源
- JTA/分布式事务下 rollback-only 语义与本地事务不同,需按对应规范判断
相关
- 关联案例:UnexpectedRollbackException 提交期 rollback-only 排查
- D2:内层异常被吞掉,是产生 rollback-only 的常见前置动作
- C1:确需子操作不污染主事务时,用 REQUIRES_NEW 而非吞异常
