StackTraces
Spring Boot已驗證

Spring Boot 新執行緒與 @Async 裡事務不傳播

@debug_master27 天前更新閱讀 6 min00

事務是綁定在執行緒上的,不是綁定在方法調用鏈上的。TransactionSynchronizationManager 用 ThreadLocal 存事務上下文,new Thread / @Async 裡的程式碼天然拿不到外層事務——子執行緒裡的寫操作會各自開一個獨立事務,外層回滾時它們紋絲不動。

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

  • 技術棧:Spring Boot 2.x / 3.x、Spring Framework 聲明式事務(@Transactional)、多執行緒併發(new Thread / @Async / 自定義執行緒池)
  • 業務場景:在主訂單事務方法中,為了優化性能或解耦,將某些寫入操作(如站內信、操作審計記錄、消息表持久化)丟入子執行緒或異步任務中執行,以為它們共處於同一個事務中。
  • 故障特徵:當主業務後續步驟發生未捕獲例外導致主事務全部回滾時,子執行緒所執行的數據庫寫操作依然停留在數據庫中,未發生回滾,造成嚴重的數據不一致和髒數據殘留。

StackTrace(堆疊日誌)

該場景下程式碼不報框架例外,但開啟 DataSourceTransactionManager 的 DEBUG 日誌後,可捕獲到關鍵的事務綁定痕跡:

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

控制台日誌截取:

// 1. 主线程开启外层事务
2026-09-14 10:15:30 DEBUG o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

// 2. 子线程执行写操作:并非加入现有事务,而是创建了全新的独立事务
2026-09-14 10:15:30 DEBUG o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
// 注意:如果是同一个事务,此处日志应当是 "Participating in existing transaction"

// 3. 子线程先提交
2026-09-14 10:15:31 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction commit

// 4. 主线程异常触发回滚
2026-09-14 10:15:32 DEBUG o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback

日誌確鑿證據:子執行緒記錄為 Creating new transaction 而不是 Participating in existing transaction,證實兩者的數據庫連接完全獨立。

Reproduce(復現程式碼)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private MessageMapper messageMapper;

    @Autowired
    private InventoryService inventoryService;

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

        // 2. 错误做法:以为子线程能共享外层事务
        new Thread(() -> {
            messageMapper.insert(buildMessage(order));
        }).start();

        // 3. 模拟扣减库存并发生异常
        inventoryService.deduct(order.getSkuId(), order.getCount());
        throw new RuntimeException("测试主事务异常回滚");
    }

    private Message buildMessage(Order order) {
        return new Message(order.getId(), "订单创建成功");
    }
}

執行結果:

  • 主表 order 插入回滾。
  • 子執行緒執行的 message 插入成功提交併永久留存在數據庫中。

Root Cause(根因分析)

Spring 聲明式事務的底層資源綁定機制如下:

TransactionInterceptor 开启事务
  → DataSourceUtils.getConnection() 从 TransactionSynchronizationManager 获取绑定资源
  → 事务上下文(Connection、rollback-only 标记等)全部保存在 ThreadLocal 中
  → 同一线程内的所有数据库操作共享同一数据库连接,保证 ACID

ThreadLocal 是“每個執行緒獨有一份副本”的存儲模型:

  1. 子執行緒無法直接繼承或訪問父執行緒的 ThreadLocal 變數。
  2. 數據庫事務本質上對應一個物理數據庫連接(Connection),JDBC 規範不支持多個執行緒同時併發交織使用同一個連接執行事務操作。
  3. Spring 官方從未提供跨執行緒傳遞活躍數據庫事務的機制。子執行緒調用任何 DAO 操作時,由於檢測不到當前執行緒的事務綁定,會獨立申請連接並單獨管理事務生命週期。

程式碼靜態審查手段: 使用 grep 排查業務 Service 事務方法中是否存在直接開啟子執行緒或異步任務的危險模式:

grep -rn -B2 "new Thread\|CompletableFuture.runAsync\|taskExecutor.execute\|@Async" src/main/java --include="*Service*"

The Fix(解決方案)

方案 A(推薦):將事務性寫操作保留在主執行緒

如果數據必須與主事務保持強一致性原子提交與回滾,必須保留在主執行緒執行;子執行緒只負責純非事務的只讀、計算、RPC、推送等操作:

@Transactional
public void createOrder(Order order) {
    // 事务写入留在主线程
    orderMapper.insert(order);
    messageMapper.insert(buildMessage(order));
    inventoryService.deduct(order.getSkuId(), order.getCount());

    // 真正的异步任务仅执行非事务通知或旁路计算
    notifyExecutor.execute(() -> pushNotification(order));
}

方案 B:子執行緒使用 TransactionTemplate 管理獨立事務

如果業務明確允許子任務獨立提交(例如落審計日誌或埋點),使用 TransactionTemplate 顯式包裹子執行緒操作,並做好失敗兜底補償:

@Autowired
private TransactionTemplate transactionTemplate;

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

    asyncExecutor.execute(() -> {
        transactionTemplate.executeWithoutResult(status -> {
            messageMapper.insert(buildMessage(order));
        });
    });
}

注意:此方案下兩者解耦,外層回滾絕不會聯動子執行緒回滾。

方案 C:基於事務同步回調(TransactionSynchronizationManager)

若異步任務依賴主事務必須成功提交後才觸發,使用事務提交回調:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    inventoryService.deduct(order.getSkuId(), order.getCount());

    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // 只有主事务确认 commit 成功后,才触发异步线程
            asyncExecutor.execute(() -> messageService.sendNotice(order));
        }
    });
}

總結與避坑要點

  1. 事務綁定執行緒而非方法調用鏈路,ThreadLocal 的物理邊界決定了跨執行緒絕不可能隱式繼承事務。
  2. 切勿嘗試用 InheritableThreadLocal 或共享 Connection 來實現跨執行緒事務,這會引發致命的連接併發死鎖與協議錯亂。
  3. 牢記原則:業務強一致性語義優先於所謂的併發性能優化。

References(參考來源)

最後更新於 2026-09-14

這篇幫到你了嗎?

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

貢獻一條解法