StackTraces
Spring Boot已驗證

Spring Boot 報 UnexpectedRollbackException

@debug_master27 天前更新閱讀 6 min00

UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only 通常不是業務根因,而是哨兵信號。在預設 REQUIRED 傳播機制下,內層方法拋出例外時 Spring 已將底層共享物理事務標記為 rollback-only;即便外層方法將該例外 try-catch 吞掉並嘗試提交,事務管理器在提交階段也會校驗該標記並強制回滾,最終拋出哨兵例外。排查該問題的關鍵在於向上追溯日誌中標記 rollback-only 的前一幀調用鏈。

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

環境設定

  • Spring Boot: 2.7.x / 3.x
  • Spring Framework: 5.3.x / 6.x
  • 事務管理: DataSourceTransactionManager / JpaTransactionManager

業務場景

業務層通常包含多步驟協同邏輯(例如創建訂單並贈送積分、發放優惠券)。開發人員為了提高主鏈路可用性,常常對外圍次要業務進行“容錯降級”處理,期望即便次要邏輯失敗,主邏輯仍然能夠正常提交:

外层 @Transactional(REQUIRED)
  → 开启物理事务,绑定底层连接 Connection A
  → 执行主业务逻辑(插入订单)
  → 调用内层 @Transactional(REQUIRED,默认传播行为)
      → 加入同一物理事务(复用 Connection A)
      → 内层逻辑异常(如积分服务不可用)
      → TransactionInterceptor 捕获异常,将 Connection A 标记为 rollback-only,并向外抛出
  → 外层使用 try-catch 拦截并吞掉内层异常,方法正常返回
  → 事务拦截器完成调用,触发 Commit 操作
  → 事务管理器检测到 rollback-only 标记
      → 无法提交,强制回滚并抛出 UnexpectedRollbackException

在預設的 REQUIRED 傳播機制下,內外層邏輯共享同一個底層物理數據庫連接。只要任何內層邏輯觸發了回滾條件,整個物理事務就被“汙染”,外層的單純 catch 無法撤銷底層連接已設置的只回滾狀態。

StackTrace(堆疊日誌)

運行時報錯並非內層拋出的具體業務例外,而是事務管理器在提交時拋出的例外:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:797)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:734)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at com.example.service.OrderService$$EnhancerBySpringCGLIB$$8b6b249a.createOrder(<generated>)

開啟 Spring 事務調試日誌後,可以觀察到關鍵的狀態流轉時序證據:

o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.service.PointService.gift] after throwing: java.lang.RuntimeException: point service unavailable
o.s.t.i.TransactionInterceptor : Participating transaction failed - marking existing transaction as rollback-only
o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.service.OrderService.createOrder] after return: null
o.s.jdbc.datasource.DataSourceTransactionManager : Committing transaction
o.s.t.i.TransactionInterceptor : Transaction rolled back because it has been marked as rollback-only

日誌明確顯示:內層 PointService.gift 拋出例外後觸發了 Participating transaction failed - marking existing transaction as rollback-only;而外層 OrderService.createOrder 吞掉例外後以 after return: null 正常返回,驅動事務提交,從而在提交階段遭遇衝突。

Reproduce(復現程式碼)

以下是典型的內外層嵌套事務錯誤容錯寫法:

package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private PointService pointService;

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);

        try {
            // 内层方法使用默认的 Propagation.REQUIRED,加入当前物理事务
            pointService.gift(order.getUserId(), 100);
        } catch (Exception e) {
            // 误区:试图在此处吞掉异常,继续向下执行并期望订单成功提交
            log.warn("积分赠送失败,降级忽略并继续下单流程", e);
        }

        orderMapper.updateOrderStatus(order.getId(), "CREATED");
    }
}

@Service
public class PointService {

    @Autowired
    private PointMapper pointMapper;

    @Transactional
    public void gift(Long userId, int point) {
        pointMapper.insertPointRecord(userId, point);
        // 模拟内层抛出运行时异常
        throw new RuntimeException("point service unavailable");
    }
}

Root Cause(根因分析)

1. Spring 嵌套事務機制設計原理

在 AbstractPlatformTransactionManager 中,內層邏輯通過 REQUIRED 傳播行為加入已存在的事務時,Spring 將其視為同一個物理事務的一個“邏輯參與者”(Participating Transaction)。

當內層邏輯拋出未捕獲的運行時例外時,AOP 攔截器 TransactionAspectSupport.completeTransactionAfterThrowing() 會被觸發。此時因為當前不是最外層事務(!status.isNewTransaction()),事務管理器無法執行直接回滾,而是調用 setRollbackOnly():

// AbstractPlatformTransactionManager.java 核心逻辑片段
if (status.isNewTransaction()) {
    doRollback(status);
} else if (status.hasTransaction()) {
    if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
        doSetRollbackOnly(status); // 标记全局连接 rollback-only
    }
}

2. 為什麼不可盲目提交?

Spring 的這一設計是為了維護 ACID 中的原子性(Atomicity)。如果允許外層 catch 吞掉內層事務失敗並照常提交,就會導致內層方法修改的數據或者執行失敗的狀態在數據層面出現不一致(或者部分生效部分丟失)。

3. 定位排查手段

排查此類問題的核心在於“抓出是哪一個內層方法首次標記了 rollback-only”以及“哪一個外層方法吞掉了例外”:

  1. 開啟事務層日誌級別: 在 application.properties 或 logback-spring.xml 中設定:
    logging.level.org.springframework.transaction=DEBUG
    logging.level.org.springframework.jdbc.datasource=DEBUG
  2. 檢索例外調用鏈與捕獲點: 在日誌中逆向搜索 Participating transaction failed,定位引發回滾的具體內層業務類與方法名。 在源碼中排查潛在吞掉例外的攔截點:
    # 检索 Service 层中包裹内层调用的 catch 块
    grep -rn -B1 "catch (Exception" src/main/java --include="*Service*"

The Fix(解決方案)

方案 A(推薦):消除吞例外邏輯,保持事務原子性

如果業務要求訂單與積分必須強一致,最清晰的做法是移除外層的 try-catch 攔截,讓例外自然向外層冒泡,由最外層事務統一觸發完整回滾:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    // 移除 try-catch,保持整体事务原子性,失败则全量回滚
    pointService.gift(order.getUserId(), 100);
    orderMapper.updateOrderStatus(order.getId(), "CREATED");
}

方案 B:確需失敗容錯時,為內層設定獨立事務(REQUIRES_NEW)

如果業務明確允許“贈送積分失敗不影響主訂單創建”,內層方法必須聲明為 REQUIRES_NEW,掛起當前事務並啟用獨立的數據庫物理連接進行提交/回滾:

@Service
public class PointService {

    @Autowired
    private PointMapper pointMapper;

    // 使用独立的物理事务,内层回滚不会污染外层的 Connection
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void gift(Long userId, int point) {
        pointMapper.insertPointRecord(userId, point);
        throw new RuntimeException("point service unavailable");
    }
}
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        try {
            // 内层走独立事务,内层回滚后外层 catch 能够正常容错且不影响外层提交
            pointService.gift(order.getUserId(), 100);
        } catch (Exception e) {
            log.warn("赠送积分失败,降级忽略", e);
        }
        orderMapper.updateOrderStatus(order.getId(), "CREATED");
    }
}

注意:使用 REQUIRES_NEW 時外層和內層各持有一個數據庫連接,高併發場景下需評估數據庫連接池承載能力,防止連接池耗盡死鎖。

方案 C:異步解耦(基於事務同步器或事件驅動)

如果贈送積分屬於非核心旁路邏輯,更優解是解耦到事務提交後異步執行:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    orderMapper.updateOrderStatus(order.getId(), "CREATED");

    // 事务成功提交后再异步触发积分赠送,彻底规避事务耦合
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            pointAsyncExecutor.giftAsync(order.getUserId(), 100);
        }
    });
}

避坑總結

  • UnexpectedRollbackException 是提交階段的“受害者提示”,真正的“案發現場”在日誌中首次出現 Participating transaction failed - marking existing transaction as rollback-only 的地方。
  • 在 REQUIRED 傳播機制下,內外層共享同一個底層連接,try-catch 無法清除已經存在的 rollback-only 標記。
  • 需要部分容錯時,必須使用 REQUIRES_NEW 獨立事務或基於 afterCommit 事件進行解耦。

References(參考來源)

最後更新於 2026-09-14

這篇幫到你了嗎?

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

貢獻一條解法