安全重試需要服務把同一個業務意圖與它的狀態變更一致地關聯,冪等鍵只是識別意圖的材料。 真正的問題是:程序可能在任何一步停止時,去重紀錄與業務效果是否仍然一致?
本文接續部分失敗與故障模型。對外 API 的方法語意、保留期與錯誤表達已整理於逾時、重試與冪等性;這裡聚焦服務內部的執行邊界。
查證日期:2026-11-04。本文只做官方文件查證,未執行資料庫、並行或故障測試。流程與資料名稱均為自創設計例子,不是可直接部署的 SQL 或框架範例。
先區分意圖與傳送嘗試
沿用文章匯出服務。使用者按一次「建立匯出」,呼叫端產生操作識別 export-intent-7,第一次送出逾時,第二次送出仍用同一操作識別。兩次網路嘗試可以有不同 request ID,方便追蹤;服務則要辨認它們代表同一意圖。
若使用者稍後真的要再匯出一次,即使文章清單與格式完全一樣,也應是另一個意圖,使用另一個識別。反過來,同一識別卻改變文章清單,也不能默默被當成原工作的合法重試。AWS 的冪等 API 說明以呼叫者提供的識別碼區分意圖,並討論相同識別、不同參數的情況。參考 AWS:Making retries safe with idempotent APIs。
本例把鍵的範圍定為「已驗證使用者+建立匯出操作+操作識別」,並保存規範化後的相關參數供比較。使用者身分由可信任的驗證結果取得,不能相信請求自行宣稱的使用者 ID;API 不能讓另一個使用者靠猜鍵取得原工作的結果。身分驗證與存取控制的背景見認證與授權。
兩種直覺流程都有缺口
先不考慮並行,假設服務能分別寫「操作識別紀錄」與「工作資料列」。若它們是兩個獨立提交,順序怎麼排都有可能留下不一致。
| 做法 | 停止的位置 | 重新送出後的風險 |
|---|---|---|
| 先提交鍵紀錄,再建立工作 | 鍵已存在,工作尚未建立 | 若把「有鍵」當成已成功,會回報不存在或未完成的結果 |
| 先提交工作,再保存鍵紀錄 | 工作已建立,鍵尚未保存 | 重試看不到鍵,可能再建立一個工作 |
這是兩筆獨立提交的故障窗口:某個程序停止點會暴露一半完成的狀態。把第二步放在 finally、調整兩步順序,或只在記憶體存一份字典,都不會讓它們自動成為同一件原子操作。持久狀態與記憶體狀態的差別,正是 crash-recovery 模型需要說明的條件。
在同一個原子邊界內保存結果
對「鍵紀錄與工作資料都在同一個支援交易的資料庫」這個有限情境,可以把它們放在同一筆交易。以下是概念流程,實際衝突處理依資料庫的隔離與錯誤規則實作:
- 驗證呼叫者、輸入與鍵的作用範圍,開始資料庫交易。
- 嘗試建立有唯一約束的操作紀錄,記錄範圍、鍵與參數。
- 若是新意圖,在同一交易建立
job-42,並保存此鍵對應的工作識別與可重現的建立結果。 - 交易成功提交後才回覆;提交失敗則依錯誤與交易狀態處理,不能回報建立成功。
- 若已存在同範圍的鍵,確認原參數相同,再回傳已提交的工作結果;若不同則依契約拒絕。
唯一約束負責在資料庫邊界裁決同鍵的競爭;不能只做「先 SELECT 不存在,再 INSERT」卻沒有約束或其他等價同步機制。資料庫約束的責任見約束與資料完整性,交易原子性見交易與隔離。
AWS 的設計說明同樣要求,保存識別資訊與相關變更必須作為具原子性等保證的操作。上面的表格與流程是本站針對單資料庫匯出服務的推演,不代表任意兩個儲存系統都能放進這個邊界。參考 AWS:Making retries safe with idempotent APIs,原子性討論。
用停止點檢查承諾
在本例的資料庫交易與持久性假設成立時,可逐步檢查:
| 停止點或不確定狀態 | 已提交狀態 | 恢復後應如何處理 |
|---|---|---|
| 提交前 API 程序停止 | 交易最終回復時,兩者皆不存在 | 使用原鍵重試,建立工作 |
| 資料庫已提交,API 尚未回應就停止 | 鍵與工作皆存在 | 原鍵重試取得同一工作 |
| API 送出 COMMIT 後與資料庫斷線 | API 尚不能確定是否提交 | 恢復連線後依鍵核對,不憑斷線宣稱 rollback |
| 回應成功送出,但呼叫端未收到 | 鍵與工作皆存在 | 原鍵重試取得同一工作 |
第三列提醒:把操作放進交易,解決的是資料狀態的原子性,不是保證所有參與者立刻知道交易結果。這仍是第一篇的「實際狀態」與「已知資訊」之別。
同鍵兩個請求同時抵達時,一個交易可能等待、發生衝突或需要重試;精確行為依產品與隔離層級而定。服務應讀取已提交結果或回報明訂的「仍在處理」,不能讀取未提交的鍵紀錄就宣稱工作已建立。本文沒有宣稱所有資料庫都能在衝突後沿用原交易繼續查詢。
本地工作不重複,不代表外部效果不重複
假設建立工作後,API 又呼叫外部通知服務寄出「已受理」信件。信件成功送出但 API 未記下結果就停止,恢復後再寄一次,仍可能產生兩封信。
資料庫 rollback 無法收回已寄出的信;在資料庫交易內進行遠端呼叫,也不會把對方加入本地交易。要承諾通知效果,必須另有通知識別、對方支援的冪等契約,或接受重複並設計可容忍的內容。後續的 Outbox 與投遞語意篇會處理這個缺口,但不會把所有外部效果都變成一次。
相同地,建立 job-42 只做一次,並不代表 worker 只執行一次。worker 可能因接手、重試與停止恢復而重做;寫輸出檔、更新完成狀態與通知,各自需要明確的所有權與副作用規則。此處先限制承諾為「建立工作」的原子邊界。
保留期與恢復策略也是正確性的一部分
去重紀錄若已清除,舊鍵晚到就可能被當成新意圖。契約需說明保留多久、何時開始計時,以及原工作仍存在時如何核對;不能把短期去重宣稱為永久業務唯一性。
若業務要求同一來源批次永遠只能建立一份工作,應將這項不變式表達為持續存在的業務規則,例如適當範圍的唯一業務鍵,而不是期待已刪除的重試 token 繼續生效。這是依有限保留條件推出的設計判斷;保留期與晚到請求的來源背景見 AWS:Late arriving requests。
本例讓重送不再任意建立新工作,但每次重試仍消耗連線、查詢與計算資源。當服務已經很慢,安全重試也可能讓它更慢;下一篇討論過載、時間預算與背壓。
相關文件
- 部分失敗與故障模型:回查逾時與提交結果的不確定性。
- Webhook 與事件回呼:辨認事件識別與重送契約。
- 分散式系統知識索引:查看完整閱讀順序。