事務是綁定在執行緒上的,不是綁定在方法調用鏈上的。
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 是“每個執行緒獨有一份副本”的存儲模型:
- 子執行緒無法直接繼承或訪問父執行緒的
ThreadLocal變數。 - 數據庫事務本質上對應一個物理數據庫連接(
Connection),JDBC 規範不支持多個執行緒同時併發交織使用同一個連接執行事務操作。 - 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));
}
});
}
總結與避坑要點
- 事務綁定執行緒而非方法調用鏈路,ThreadLocal 的物理邊界決定了跨執行緒絕不可能隱式繼承事務。
- 切勿嘗試用 InheritableThreadLocal 或共享 Connection 來實現跨執行緒事務,這會引發致命的連接併發死鎖與協議錯亂。
- 牢記原則:業務強一致性語義優先於所謂的併發性能優化。
