首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring 事務傳播機制實戰避坑:巢狀呼叫、自調用與異常回滾的完整解決方案

     編輯:Java 綜合教學

問題場景重現

// Service A
@Service
public class AService {
    @Autowired private BService bService;
    @Autowired private ADao aDao;

    @Transactional
    public void a() {
        aDao.updateA();
        b(); // 內部呼叫
    }

    @Transactional
    public void b() {
        aDao.updateB();
        cService.c(); // 跨 Service 呼叫
    }
}

// Service C
@Service
public class CService {
    @Autowired private CDao cDao;
    @Autowired private DService dService;

    @Transactional(rollbackFor = MyException.class)
    public void c() {
        cDao.updateC();
        dService.d(); // 拋出 MyException
    }
}

// Service D
@Service
public class DService {
    @Autowired private DDao dDao;

    @Transactional(rollbackFor = MyException.class)
    public void d() {
        dDao.updateD();
        throw new MyException(); // 自訂 SQLException 子類
    }
}

期望:d() 拋出 MyException 時,d()、c() 回滾;a()、b() 不回滾。

方案嘗試與失敗分析

1. noRollbackFor 失效

@Transactional(noRollbackFor = MyException.class)
public void a() { ... }

結果:四個方法全部回滾。
原因:Spring 在容器啟動時解析所有 @Transactional 方法的 rollbackRules,運行時 TransactionAspectSupport.invokeWithinTransaction 捕獲異常後,直接調用 completeTransactionAfterThrowing 進行全局回滾判斷,noRollbackFor 僅影響當前代理方法的規則,無法阻止外層事務傳播的回滾決策。

2. PROPAGATION_REQUIRES_NEW 失效

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = MyException.class)
public void c() { ... }

結果:d() 根本未執行。
原因:REQUIRES_NEW 需要透過代理物件呼叫才能開啟新事務。若 b() 直接呼叫 c()(this 呼叫),繞過代理,新事務不生效,仍在外層事務中運行;若 c() 內捕獲異常並吞沒,外層事務不回滾,但 d() 還是執行了——實測發現 d() 未執行,推測為事務管理器檢測到嵌套新事務與現有事務衝突導致異常提前終止。

根本原因:Spring 事務本質是「單一資料庫連線的邊界控制」

  • 預設 PROPAGATION_REQUIRED:所有巢狀呼叫共享同一個 Connection、同一個事務。
  • 一旦任意節點標記 rollbackOnly = true,整個連線的事務必須回滾,無法部分提交。
  • noRollbackFor 僅決定「是否標記 rollbackOnly」,無法改變「共享連線」的物理事實。

三種可行解決方案

方案一:手動控制事務(最靈活)

@Autowired private PlatformTransactionManager txManager;

public void a() {
    // 外層不開啟事務,或開啟 REQUIRED
    TransactionTemplate template = new TransactionTemplate(txManager);
    template.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW);
    template.execute(status -> {
        try {
            cService.c();
        } catch (MyException e) {
            status.setRollbackOnly(); // 僅回滾內層
            throw e;
        }
        return null;
    });
    // 繼續外層邏輯,不受影響
}

方案二:服務拆分 + 獨立事務邊界(推薦架構層面)

  • AService、BService 使用一個事務管理器(或無事務)。
  • CService、DService 獨立部署為微服務,或至少獨立資料源 + 獨立事務管理器。
  • 跨服務呼叫透過 REST/gRPC,物理隔離事務,天然實現部分回滾。

方案三:業務邏輯重構,避免「大事務包小事務」反模式

  • 將 d() 的高風險操作(如外部 API 呼叫、不確定計算)前置驗證,失敗拋業務異常而非 SQL 異常。
  • 採用「補償事務」模式:d() 失敗時,記錄失敗日誌,由排程任務異步補償/回滾相關資料,而非依賴資料庫事務回滾。
  • 將 MyException 改為非 SQLException 子類(如繼承 RuntimeException),並精確控制 rollbackFor。

最佳實踐檢查清單

  1. 預設使用 @Transactional(rollbackFor = Exception.class),除非明確不需回滾。
  2. 避免同類內部呼叫 this.transactionalMethod(),改用 AopContext.currentProxy() 或拆分 Bean。
  3. 多資料源場景必須指定 @Transactional("specificTxManager")。
  4. 長事務、跨服務呼叫、外部 API 呼叫嚴禁放在事務內。
  5. 單元測試覆蓋事務邊界:正常提交、異常回滾、巢狀傳播、只讀優化。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved