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 的獨立性是雙向的:
- 持久化不可逆:內層事務先行提交後,數據庫底層已經完成寫盤與鎖釋放,無法通過外層回滾進行數據撤銷。
- 讀未提交不可見:內層事務在新連接中執行,預設隔離級別(如 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);
}
}
總結與避坑要點
REQUIRES_NEW獨立提交是雙刃劍,它不是解決回滾問題的萬能補丁,往往是引入數據不一致的源頭。- 外層回滾無法撤回已提交的內層事務,這是 JDBC 與數據庫 ACID 的基本規律而非框架 Bug。
- Code Review 時,對所有標記為
REQUIRES_NEW的方法必須追問:“外層回滾時,這裡的持久化數據單獨保留是否合法?”。
