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 而非吞例外
