StackTraces
Spring Boot已驗證

Spring Boot 事務內 try-catch 吞例外不回滾

@debug_master27 天前更新閱讀 6 min00

回滾由 TransactionInterceptor 捕獲「穿過代理邊界的例外」觸發。事務方法內部 try-catch 把例外吞掉後正常返回,代理感知不到任何例外,事務照常提交——這是事務失效裡最常見也最反直覺的一種,因為方法確實“報錯了”,事務卻“沒壞”。

Issue & Env(案發現場與環境)

  • 技術棧:Spring Boot 2.x / 3.x、Spring Framework 聲明式事務(@Transactional)
  • 業務場景:下單主流程在插入訂單記錄後,調用下游扣庫存或發積分等子操作。開發者用 try-catch 包裹了子調用以打印錯誤日誌或做局部防禦,但未重新拋出例外。
  • 故障特徵:業務日誌中明確打印了 ERROR: 库存扣减失败,然而下游庫存雖然未扣除成功,前置插入的 order 訂單記錄與積分卻依然被持久化提交到了數據庫,未發生預期內的全量事務回滾。

StackTrace(堆疊日誌)

本故障表面上沒有未捕獲例外拋出,控制台通常輸出業務自定義的錯誤日誌,緊隨其後的是事務正常提交的跟蹤記錄:

2026-09-14 10:02:11 ERROR c.e.OrderService : 库存扣减失败: insufficient stock
2026-09-14 10:02:11 DEBUG o.s.t.interceptor.TransactionInterceptor : Completing transaction for [OrderService.createOrder] after return: null
//                                                                                                ^^^^^^^^^^^^^^ after return,不是 after throwing

開啟 Spring 事務詳細調試日誌可清楚觀察到提交決策:

logging.level.org.springframework.transaction.interceptor=DEBUG

關鍵證據對比:

  • Completing transaction ... after return:代理攔截器收到的是方法正常返回,走成功提交流程。
  • Completing transaction ... after throwing: xxxException:只有例外冒泡到代理層時,才會進入回滾判定鏈。

Reproduce(復現程式碼)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private PointService pointService;

    @Transactional
    public void createOrder(Order order) {
        // 1. 插入订单
        orderMapper.insert(order);

        // 2. 扣库存,被 catch 住且未重新抛出
        try {
            inventoryService.deduct(order.getSkuId(), order.getCount());
        } catch (BusinessException e) {
            log.error("库存扣减失败: {}", e.getMessage()); // 仅记日志,吞掉异常
        }

        // 3. 正常执行后续逻辑并提交
        pointService.add(order.getUserId(), order.getPoint());
    }
}

觸發調用後,inventoryService.deduct 拋出 BusinessException,但被內部消化,外部觀測發現 order 成功落庫。

Root Cause(根因分析)

Spring 聲明式事務的回滾判定鏈路高度依賴 AOP 代理層的攔截:

业务方法抛出异常
  → 代理层 TransactionInterceptor.invokeWithinTransaction() 捕获
  → TransactionAspectSupport.completeTransactionAfterThrowing()
  → 按 rollbackFor 规则匹配(默认针对 RuntimeException 和 Error)
  → rollback()

整條鏈路的前提只有一個:例外必須穿透方法體並拋至代理層(Proxy Boundary)。

  • @Transactional 是環繞通知包裝器,無法窺探方法內部執行流程。
  • 當方法體內自行執行了 try-catch 且沒有 throw,Java 方法調用視為“正常返回(Return Normally)”。
  • 代理層攔截器只能觸發 completeTransactionAfterReturning(),從而執行數據庫連接提交。

快速定位排查: 使用 grep 快速排查業務層中所有隻打印日誌而不拋出例外的 catch 塊:

grep -rn -A2 "catch.*Exception" src/main/java --include="*Service*" | grep -B1 "log.error"

The Fix(解決方案)

方案 A(推薦):catch 後重新拋出例外

如果需要記錄業務上下文日誌,可以保留 catch 塊,但在記錄後必須將例外繼續向外拋出(或轉換為受支持的運行時例外):

try {
    inventoryService.deduct(order.getSkuId(), order.getCount());
} catch (BusinessException e) {
    log.error("库存扣减失败: {}", e.getMessage());
    throw new RuntimeException("库存扣减失败,触发回滚", e); // 重新抛出,让代理感知
}

方案 B:catch 內手動標記回滾狀態(setRollbackOnly)

對於確實需要執行降級返回、不能直接對外拋出例外中斷響應的場景,可以在 catch 塊內顯式將當前事務標記為回滾狀態:

try {
    inventoryService.deduct(order.getSkuId(), order.getCount());
} catch (BusinessException e) {
    log.error("库存扣减失败,标记事务回滚: {}", e.getMessage(), e);
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

注意:setRollbackOnly() 會使當前事務在方法執行完畢後強制回滾,即便後續方法正常 return。

方案 C:將非關鍵子操作拆分為獨立事務(REQUIRES_NEW)

如果子操作是邊緣日誌或非核心通知,即便失敗也不應當影響主訂單事務,應將該方法拆分至獨立 Bean 並聲明為獨立事務:

@Service
public class AuditLogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordLog(LogEntity log) {
        // 独立事务,失败自生自灭,不污染主事务
    }
}

總結與避坑要點

  1. 事務回滾的硬性前置條件是“例外穿過代理邊界”,內部 catch 是對代理的“報喜不報憂”。
  2. catch 塊內要麼重新拋出例外,要麼調用 setRollbackOnly(),絕不可靜默吃掉。
  3. Code Review 時重點關注所有 @Transactional 方法內的 catch 塊,確認其例外傳遞路徑是否符合事務原子性預期。

References(參考來源)

最後更新於 2026-09-14

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法