StackTraces
Spring Boot已驗證

Spring Boot REQUIRES_NEW 內層提交致外層回滾髒數據

@debug_master27 天前更新閱讀 6 min00

REQUIRES_NEW 的語義是「掛起外層事務、開一個全新的事務,互不干擾」——獨立是雙向的:外層回滾救不了已提交的內層,內層也看不到外層未提交的數據。把主業務數據放進 REQUIRES_NEW,就是親手製造“部分提交”的髒數據。

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

  • 技術棧:Spring Boot 2.x / 3.x、Spring Framework 聲明式事務(@Transactional(propagation = Propagation.REQUIRES_NEW))
  • 業務場景:在主訂單流程中,調用內部某服務記錄操作日誌、審計軌跡或發放附屬資產。開發者將內層方法標記為 REQUIRES_NEW,期望子調用獨立完成。
  • 故障特徵:當下遊業務發生未處理例外導致主事務發生回滾時,外層的主訂單數據回滾消失,而內層通過 REQUIRES_NEW 插入的記錄卻已真實提交併永久留存,形成了業務上不可接受的孤兒髒數據。

StackTrace(堆疊日誌)

該問題在業務邏輯上體現為數據不一致,程式碼可能拋出業務例外或正常報錯。開啟 Spring 事務 DEBUG 級別日誌後可清晰觀察到事務的生命週期分歧:

logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

關鍵執行軌跡如下:

// 1. 外层开启主事务
2026-09-14 10:20:01 DEBUG o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [OrderService.createOrder]

// 2. 遇到 REQUIRES_NEW:挂起外层事务,申请新连接创建全新独立事务
2026-09-14 10:20:01 DEBUG o.s.j.d.DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [AuditService.record]
//                                                                ^^^^^^^^^^ 挂起当前事务,开辟新事务

// 3. 内层方法执行完毕:立即真实提交
2026-09-14 10:20:02 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction commit
2026-09-14 10:20:02 DEBUG o.s.j.d.DataSourceTransactionManager : Resuming suspended transaction after completion of inner transaction

// 4. 外层后续抛出异常:主事务触发回滚,但已无法追溯已提交的内层数据
2026-09-14 10:20:03 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
2026-09-14 10:20:03 DEBUG o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [...]

日誌標誌:Suspending current transaction, creating new transaction 是判定事務被切分為兩段不同物理連接的鐵證。

Reproduce(復現程式碼)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private AuditService auditService;

    @Autowired
    private InventoryService inventoryService;

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

        // 调用 REQUIRES_NEW 的独立事务
        auditService.record(order, "CREATE_ORDER");

        // 模拟后续业务抛出未捕获异常
        inventoryService.deduct(order.getSkuId(), order.getCount());
        throw new RuntimeException("库存扣减异常,主事务触发回滚");
    }
}

@Service
public class AuditService {

    @Autowired
    private AuditMapper auditMapper;

    // 错误地将与主业务强相关的操作配置为 REQUIRES_NEW
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(Order order, String action) {
        auditMapper.insert(new AuditLog(order.getId(), action));
    }
}

執行後數據庫狀態:

  • orders 表中無新記錄(已回滾)。
  • audit_log 表中出現了一條指向不存在訂單 ID 的孤立記錄(已提前提交)。

Root Cause(根因分析)

Spring 事務傳播行為中 REQUIRED 與 REQUIRES_NEW 的根本分水嶺在於物理數據庫連接(Connection)的生命週期與隔離:

REQUIRED(默认):
  当前存在事务 → 直接加入,全流程共享同一数据库物理连接与同一个 Commit/Rollback 终点。
  内层方法退出并不触发物理提交。

REQUIRES_NEW:
  当前存在事务 → 挂起当前事务上下文,从连接池借出全新的物理连接,开辟独立事务单元。
  内层方法退出时立即执行物理 commit。
  外层后续无论回滚与否,均无法撤销另一个已经提交的数据库连接所写入的数据。

REQUIRES_NEW 的獨立性是雙向的:

  1. 持久化不可逆:內層事務先行提交後,數據庫底層已經完成寫盤與鎖釋放,無法通過外層回滾進行數據撤銷。
  2. 讀未提交不可見:內層事務在新連接中執行,預設隔離級別(如 READ COMMITTED)下,內層根本查詢不到外層剛插入但尚未提交的髒數據,容易引起二次主外鍵衝突或查詢為空錯誤。

排查定位手段: 使用 grep 檢索工程內所有的 REQUIRES_NEW 聲明:

grep -rn "REQUIRES_NEW" src/main/java

針對每一處進行業務語義審視:該方法寫入的數據在外層回滾後如果仍然存在,是否會導致系統髒數據。

The Fix(解決方案)

方案 A(推薦):業務數據強一致 → 改回預設 REQUIRED 傳播級別

如果寫入的數據必須同生共死(例如訂單明細、附贈優惠券、業務流轉記錄),必須保持在同一個事務邊界內:

@Service
public class AuditService {
    // 移除 REQUIRES_NEW,继承外层事务
    @Transactional(propagation = Propagation.REQUIRED)
    public void record(Order order, String action) {
        auditMapper.insert(new AuditLog(order.getId(), action));
    }
}

外層一旦例外回滾,內層寫入的操作將一併被數據庫回滾。

方案 B:確需獨立持久化 → 嚴格隔離為純旁路記錄

如果業務確實屬於“無論主業務成功與否,必須留痕(如安全審計、操作軌跡、接口防重日誌)”的純旁路需求,使用 REQUIRES_NEW 是合理的,但不能依賴主業務主鍵外鍵關聯:

@Service
public class SecurityAuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordAccessLog(String traceId, String action, String payload) {
        // 只记录无外键强约束的无状态日志,明确接受即使主流程失败也会落库的语义
        auditMapper.insert(traceId, action, payload);
    }
}

總結與避坑要點

  1. REQUIRES_NEW 獨立提交是雙刃劍,它不是解決回滾問題的萬能補丁,往往是引入數據不一致的源頭。
  2. 外層回滾無法撤回已提交的內層事務,這是 JDBC 與數據庫 ACID 的基本規律而非框架 Bug。
  3. Code Review 時,對所有標記為 REQUIRES_NEW 的方法必須追問:“外層回滾時,這裡的持久化數據單獨保留是否合法?”。

References(參考來源)

最後更新於 2026-09-14

這篇幫到你了嗎?

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

貢獻一條解法