先讓重試沿用同一個業務身份
寫入請求逾時,不代表設備沒有執行。回覆可能在途中遺失,呼叫者若用新識別再送一次,就可能產生兩次實體動作。冪等設計先由業務建立operation_id,從第一次送出到所有重試都保持不變;封包序號或TCP連線重建不能代替它。
去重紀錄至少包含operation_id、payload hash、目前狀態、結果摘要、建立時間與重播窗口。第一次請求若成功,重送相同operation_id和相同payload應回傳原結果,不重新執行效果。相同operation_id卻帶不同payload,必須回報IdempotencyConflict,不能選一個覆蓋另一個。
hash是內容一致性的輔助證據,不是身份本身。hash計算前要固定編碼、欄位順序、單位與正規化規則;同一操作若只是JSON欄位順序不同,要由契約決定是否視為相同。未知或缺少operation_id時,不可自行用來源port或時間戳拼湊一個看似唯一的鍵。
本文只描述資料與交易設計,不假稱Q系列PLC已有冪等API、暫存器或命令完成旗標。實際設備若只接受無識別寫入,應由受控代理、結果查詢或人工流程降低重試風險。
把去重與效果放在可說明的邊界
理想流程是去重紀錄和業務效果在同一個可原子提交的transaction內:以唯一鍵或等價原子條件取得operation_id的處理權,再保存請求並執行或記錄效果;只做查無此鍵後插入仍有並行競態,最後提交結果。若效果在外部設備、PLC或另一個服務,通常無法靠本地資料庫transaction同時包住;這時要明確畫出outbox、結果回報、查詢和補償邊界。
案例:operation_id=OP7、payload hash=H7第一次送出,代理保存pending後送設備;設備已寫入但代理在保存result前崩潰。恢復時OP7是unknown,不能宣稱exactly once實體動作,也不能直接再寫一次。先用設備支援的狀態查詢、讀回版本或人工確認,才決定完成、補償或拒絕重試。
若OP7重送且結果已保存,回傳同一result_id和結果摘要;若OP7重送但H不同,立即衝突。若第一次仍pending,依規格回pending或讓查詢者等待,不要並行啟動第二個效果。每個狀態轉移要有時間、來源和操作者紀錄。
outbox可讓資料庫提交後由發送器重試同一operation_id,但外部接收端仍必須理解冪等鍵。只在本端outbox去重,不能推論遠端設備不會重複執行;接收、效果與結果查詢的責任要寫入介面契約。
若設備回覆「已接受」而非「已完成」,去重狀態只能標accepted或pending,不能直接標success。後續完成事件要以同一operation_id關聯;若設備不提供事件或查詢,逾時後就只能保留unknown。這種保守狀態可避免把排隊中的命令當成實體效果已發生。
重試請求與結果查詢也要分開權限。允許查詢不代表允許再次寫入;代理可讓同一operation_id查詢目前狀態,但拒絕沒有鍵的任意補送。若人工決定重新執行,應建立新的operation_id並保存與舊操作的關聯理由。
TTL與重播窗口不是永久保證
去重紀錄需要TTL或重播窗口,避免無限增長。窗口應大於預期最大逾時、排隊、重連與人工重試時間;但窗口過期後,舊封包可能再次到達並被視為新操作。對有副作用的write,窗口外要拒絕、要求新一輪核准或先查設備狀態,不能只因資料庫刪掉舊鍵就重做。
刪除去重鍵後若仍接受任意舊ID,就無法辨認它是否過期。需另有可驗證的操作期限、有效世代或到期墓碑政策;不能只靠已被刪除的紀錄拒絕重播。期限過後拒絕寫入,但結果查詢可依保存政策繼續提供。
重啟要恢復去重紀錄或明確標示其遺失。若只保存operation_id在易失記憶體,重啟後同一重試無法辨識;若持久化檔案可能損壞,要進入recovery而不是清空後繼續寫。容量滿時拒絕新write、保留既有鍵並告警,不能淘汰未知效果的項目。
正常案例是OP7第一次完成,兩次重送都回原結果且效果計數仍為一。失敗案例是第一次效果未知、第二次用新OP8重送,報表看似兩筆成功;排查要對照端點事件、operation_id、hash、時間和outbox狀態。
若業務允許重複但效果可合併,仍要把合併規則寫清楚,不能把非冪等write默認當成安全。查詢、讀取和純狀態刷新通常可採較寬策略;設備啟停、配方寫入或計數增加則需更嚴格確認。
在多個代理共同送出時,去重權威必須集中或有一致的分片規則。兩個代理各自保存OP7並同時送設備,單看代理內部狀態仍可能重複;日誌要帶代理識別、路由分片和中央去重結果。
hash衝突報告要保留兩次請求的摘要、長度與編碼版本,避免只顯示一個模糊的conflict。修正payload時建立新operation_id,讓舊操作的審計鏈仍可追溯。
重試策略還要限制並發與退避,避免服務恢復時大量相同操作同時重送。退避只能控制流量,不能取代operation_id去重和結果查詢與核對資料內容。
驗收與可證明的限制
離線驗收至少測:相同OP與相同hash重送、相同OP不同hash、第一次逾時後效果已完成、代理崩潰在各提交點、outbox重送、TTL前後、去重容量滿、重啟恢復、缺少operation_id與並行重送。每列記錄效果次數、狀態、結果查詢與預期處置。
只有在去重、效果與結果提交都能被同一可靠邊界涵蓋時,才可談整體exactly once;跨越外部設備且沒有查詢或事務協定時,應依實際送達契約描述是否至少一次,並將未確認的外部效果標unknown,而不是用「只送一次」的文字掩蓋風險。
operation_id不應包含秘密或可猜測的敏感資料;日誌保存hash、摘要和必要欄位即可。實際TLS、代理、PLC通訊模組和設備命令語意要查各自文件。本文案例已核對。
排查時先查operation_id與hash,再查去重狀態、outbox、端點事件和結果查詢。若只看到兩個TCP封包,不能直接推論設備執行兩次;若看到兩個不同operation_id成功,才有重複業務身份的明確線索。
常見問題
問:每次重試都要使用相同識別嗎?答:業務operation_id保持不變;每次attempt可另有追蹤識別,兩者不要混用。
問:payload hash相同就一定能重做嗎?答:hash只協助確認內容,仍要先查operation狀態與效果是否已完成。
問:代理崩潰後可以宣稱exactly once嗎?答:若外部效果不在同一可原子邊界且無查詢證據,只能標示unknown或依契約採其他語義。
問:TTL到期後舊operation可以直接當新操作嗎?答:有副作用時不應直接接受,先查設備狀態或重新取得核准。
參考:AWS Builders Library的Making retries safe with idempotent APIs,說明以請求識別與參數一致性處理重試;本文另加外部設備效果未知的限制。
參考:IETF RFC 9110的重複請求與方法語意背景,作為HTTP重試與效果語意參考;不代表PLC命令或設備API。