題干與適用場景
客戶端送出建立訂單命令,服務端可能已提交訂單,卻在回應前斷線。客戶端無法判斷結果,準備用同一個 Idempotency-Key 重試。請設計請求契約、持久化紀錄、並發控制與恢復流程,並說明哪些失敗可以安全重試。核心是副作用邊界、原子性與故障後的可解釋狀態,因此歸入 backend。
面試官考察點
高品質回答會先區分「請求未到達」「已完成但回應遺失」和「仍在處理」,再決定重播結果、查詢狀態或回覆處理中。也應涵蓋同鍵不同參數、並發首請求、儲存故障、鍵過期、跨區路由與下游副作用,而不是只說「把鍵放 Redis」。
回答前需要澄清的問題
- 冪等鍵由客戶端為一次邏輯命令產生,還是由服務端從業務欄位推導?
- 同鍵要綁定哪些請求欄位,參數不一致時回傳什麼錯誤?
- 訂單寫入與冪等紀錄是否在同一個資料庫交易?下游支付是否另有冪等能力?
- 客戶端可等待多久,鍵需保留多久,過期後重複命令是否可能產生新訂單?
- 服務是單區還是多區,冪等紀錄如何讓所有入口看到同一結果?
30 秒回答框架
「客戶端為每次邏輯命令產生穩定的冪等鍵,服務端以租戶、端點與鍵組成唯一約束,保存請求指紋、狀態與最終回應。首次請求原子地建立 processing 紀錄並執行訂單交易;同鍵同參數的並發請求等待或重播結果,同鍵不同參數立即回傳衝突。服務端崩潰後透過交易結果與逾時恢復狀態,未知的下游副作用先查詢再補償,不能盲目重試。保留期需覆蓋客戶端重試窗口,並用指標驗證重複副作用為零。」
分步驟深入解答
請求契約應要求冪等鍵非空、長度受限,且一次邏輯命令的所有重試都保持不變。服務端將 (tenantid, operation, idempotencykey) 建立唯一鍵,同時保存規範化請求雜湊、狀態、資源 ID、回應碼、回應內容與過期時間。請求雜湊可避免客戶端誤用同一鍵執行不同命令。
首次請求應先在持久化儲存插入 processing 紀錄,再與建立訂單寫入放進同一個本地交易,或使用能明確關聯兩者的交易日誌。插入衝突時讀取已有紀錄:succeeded 直接回傳保存的回應,failed 依契約回傳相同業務錯誤,processing 回覆處理中或短暫等待。不要只把鎖放在單機記憶體。
並發請求必須由唯一約束與條件更新決定勝負。只有持有建立紀錄的請求可以把狀態從 processing 改成 succeeded;更新條件包含版本號或狀態,避免兩個 worker 同時提交。若訂單交易成功而程序在回應前崩潰,重試讀取 succeeded 紀錄即可重播;若交易回滾,才可安全再次執行。
下游副作用會產生「本地未知、遠端可能成功」的窗口。支付、出貨或訊息傳送應使用相同業務操作 ID 呼叫支援冪等的下游介面,並記錄請求與結果。若下游不支援冪等,先查詢對帳介面或寫入 outbox,再由可重試 worker 發送,不能因本地逾時就再次扣款。
處理中紀錄需要恢復策略。可保存租約與最後心跳,由恢復 worker 查詢訂單交易、下游狀態或交易日誌後把紀錄推進終態;證據不足時標記 unknown 並交由人工或對帳流程處理,而不是把未知當失敗。狀態機要禁止 succeeded 回退到 processing。
鍵過期必須與業務風險匹配。保留期至少覆蓋客戶端最大重試、網路重試佇列與人工補償窗口;過期後再次使用同一鍵應回傳明確的 key_expired,不要靜默建立第二個訂單。可壓縮舊回應,但要保留稽核摘要與資源唯一約束。
跨區部署要讓同一邏輯鍵落到一致的權威儲存,或使用全域唯一約束與同步複製。讀到 processing 時不能因跨區延遲就轉到另一副本再次執行。指標至少包括鍵衝突率、參數衝突、處理中逾時、重播回應、未知狀態、重複資源攔截與下游對帳差異。
高品質示範回答
「我把冪等鍵定義成一次邏輯建立命令的穩定標識,而不是每次 HTTP 重試都新產生的請求 ID。服務端用租戶、操作類型與鍵建立持久化唯一約束,記錄請求雜湊、狀態、資源 ID、回應與過期時間。首個請求原子地取得 processing 紀錄並建立訂單;同鍵同參數的請求重播保存結果或等待,同鍵不同參數回傳衝突。
我會把訂單寫入與冪等紀錄放在同一交易,並讓支付等下游使用相同業務操作 ID。若回應遺失,重試讀取已保存結果;若本地狀態未知,先查下游與對帳紀錄,絕不靠再次 POST 猜測。worker 用租約恢復卡住的 processing,狀態只能按條件更新推進到 succeeded、failed 或 unknown。保留期覆蓋所有重試與補償窗口,過期鍵明確拒絕。壓測並發首請求、程序崩潰與跨區延遲,驗收重複訂單與重複扣款均為零。」
常見錯誤
- 每次重試產生新鍵 → 無法辨識同一邏輯命令 → 重試時沿用原鍵。
- 只用記憶體或單機鎖 → 重啟與多副本失去去重 → 使用持久化唯一約束。
- 同鍵不同參數仍回傳舊結果 → 掩蓋客戶端錯誤 → 保存請求指紋並回傳衝突。
- 記錄
processing後直接呼叫下游 → 崩潰時無法判斷副作用 → 使用交易、outbox 或下游冪等介面。 - 逾時就再次扣款 → 遠端可能已成功 → 先查詢狀態與對帳。
- 把卡住紀錄當失敗重做 → 可能產生第二資源 → 恢復 worker 先收集證據。
- 鍵太快過期 → 延遲重試建立第二訂單 → 依風險和重試窗口設定保留期。
- 只測試串行請求 → 並發競爭仍會雙寫 → 測試同鍵並發、崩潰與多區路由。
追問及應對
追問一:為什麼不能只用訂單號做唯一鍵?
訂單號通常在服務端建立後才產生,無法覆蓋首次請求尚未回應的窗口。冪等鍵先於副作用存在,並透過唯一約束把重試綁定到同一次邏輯命令。
追問二:同一鍵但請求內容變化怎麼辦?
將規範化請求體與關鍵標頭計算指紋並保存。指紋不同回傳參數衝突,不執行新副作用;客戶端應產生新鍵表示新的邏輯命令。
追問三:首個請求一直停在 processing 怎麼辦?
使用租約、心跳與逾時掃描。恢復 worker 查詢本地交易、下游結果與訊息日誌;證據足夠才推進終態,證據不足就標記 unknown 並進入對帳流程。
追問四:Redis 能單獨承擔冪等紀錄嗎?
若副作用權威資料在資料庫,單獨 Redis 可能在淘汰、故障或複製延遲時丟失安全邊界。可用 Redis 做短期協調,但最終狀態與唯一約束應在能與業務寫入保持一致的持久化儲存中。
追問五:鍵過期後使用者再次提交同一訂單怎麼辦?
不要靜默接受舊鍵。回傳過期錯誤並要求客戶端查詢原訂單或產生新命令;資源層也應有業務唯一約束,防止重複結算。
追問六:如何驗證沒有重複副作用?
並發送出相同鍵、在提交前後殺程序、製造回應遺失與下游逾時,檢查訂單唯一約束、支付操作 ID、重播回應、狀態轉移日誌與對帳結果。核心斷言是每個邏輯鍵最多一次成功副作用。
追問七:冪等鍵是否等於 exactly-once?
不是。它約束同一服務辨識重複命令,不能讓跨服務網路天然 exactly-once。跨邊界仍需交易、outbox、下游冪等、查詢與對帳,並明確哪些結果可能是 unknown。