NMQ 並發 Race Condition 導致訂單子單轉單遺失分析
事件摘要
一筆 HK 市場訂單(TradesOrderGroupId: 2708256)的兩筆子單中,只有一筆成功轉單至 ERP,另一筆(TradesOrderSlaveId: 8877652)轉單失敗,導致 WebStore 的 OrderSlaveFlow 狀態永久卡在 WaitingToTrans,且 ERPDB 中完全沒有該子單的資料。
現象觀察
查詢 WebStore 的 OrderSlaveFlow,同一筆訂單的兩筆子單狀態截然不同:
| OrderSlaveFlow_Id | TradesOrderSlaveId | StatusDef | TransToERPDateTime | SalesOrderSlaveId |
|---|---|---|---|---|
| 8877547 | 8877653 | WaitingToShipping |
2026-05-07 08:16:38 | 8875983 |
| 8877546 | 8877652 | WaitingToTrans |
NULL |
NULL |
- 8877653 ✅ 正常轉單,有
SalesOrderSlaveId,狀態已推進至WaitingToShipping - 8877652 ❌ 轉單失敗,無任何 ERP 資料,狀態凍結在
WaitingToTrans
查詢 ERPDB 確認:
1 | SELECT * FROM ERPDB.dbo.OrderSlaveFlow |
轉單機制說明
轉單流程由 NMQ Job TransferOrderToERP 觸發,主要分三步:
1 | Step1: csp_ImportWebStoreDBTradesOrdersToERPDBSourceTablesByOrderId_Mall |
CSP 的關鍵 WHERE 條件
1 | FROM [WebStoreLS].WebStoreDB.dbo.OrderSlaveFlow v1 WITH (NOLOCK) |
CSP 的 CATCH block
1 | BEGIN CATCH |
關鍵時間線
1 | 08:15:58.797 兩筆 TradesOrderSlave 狀態皆變為 Finish |
Root Cause 分析
問題一:NOLOCK 髒讀(Dirty Read)Race Condition
兩個 Worker 的 CSP 幾乎同時執行,導致以下交錯:
1 | Worker 1 (08:16:38.430) Worker 2 (08:16:38.436) |
核心問題: CSP 的 NOT EXISTS 使用 WITH(NOLOCK),導致 Worker 2 讀到 Worker 1 尚未 commit 的 dirty data,誤以為 8877652 已存在而跳過,最終 Worker 1 rollback 後 8877652 在兩邊都不見了。
問題二:NMQ Retry 跳過 Step1+Step2
Worker 1 Step1 失敗後,retry 邏輯直接從 Step3 開始,跳過了 Step1(轉入 ERPDB)和 Step2(建立 SalesOrderSlave):
1 | 第一次執行:Step1 ❌ → retry |
這是 retry 機制的設計缺陷,應該從 Step1 重試而非從 Step3 跳入。
問題三:同一筆 TG 被觸發兩個 NMQ Job
同一筆 TradesOrderGroupId = 2708256 在幾乎同一時間點被發了兩個 TransferOrderToERP task,這是觸發整個 race condition 的根本原因。
為何直接 Re-run CSP 無效
SourceTradesOrderGroup 的 INSERT 沒有 NOT EXISTS 保護:
1 | INSERT INTO [ERPDB].[dbo].[SourceTradesOrderGroup] |
Re-run 時的執行流程:
1 | 1. OrderSlaveFlow INSERT → 8877652 符合條件 → 進入 @tmpOrder ✅ |
資料確認
| 資料表 | GroupId/SlaveId | 狀態 |
|---|---|---|
ERPDB.dbo.SourceTradesOrderGroup |
2708256 | ✅ 存在 |
ERPDB.dbo.SourceTradesOrderSlave |
8877653 | ✅ 存在 |
ERPDB.dbo.SourceTradesOrderSlave |
8877652 | ❌ 缺失 |
ERPDB.dbo.OrderSlaveFlow |
8877652 | ❌ 缺失 |
修復方向
短期(資料補救)
由於 CSP 整包無法重跑,需要寫 Operation SQL,僅補齊 8877652 缺失的 slave-level 資料:
ERPDB.dbo.OrderSlaveFlow(8877652)ERPDB.dbo.SourceTradesOrderSlave(8877652)- 其他 slave-level 關聯表(如
SourceTradesOrderSlavePromotion、SourceAuthorized等)
長期(根本修復)
| # | 問題 | 修復方向 |
|---|---|---|
| 1 | 同一 TG 被發兩個 NMQ job | 調查重複觸發來源,加入 job deduplication |
| 2 | CSP NOT EXISTS 使用 NOLOCK 造成髒讀 | 改用 READCOMMITTED 或加入 Application Lock |
| 3 | NMQ retry 跳過 Step1+Step2 | 修正 retry 邏輯,從失敗的 step 重試 |
| 4 | CSP group-level INSERT 缺乏冪等保護 | 加入 IF NOT EXISTS guard |
學習重點
NOLOCK 不等於「安全的讀取」
WITH(NOLOCK)會讀取其他 transaction 尚未 commit 的 dirty data。
在並發場景下,這可能導致:
- 誤判資料「已存在」而跳過(本案例)
- 讀到後來被 rollback 的資料
- 幻象讀、不一致的資料狀態
在有並發風險的去重判斷(NOT EXISTS)上,應避免使用 NOLOCK。


