重複執行,會不會多做一次?
不同操作,重複執行的影響也不同。把地址再次設成相同內容,與把庫存再扣兩件,不能用同一種方式判斷。今天回到前面談過的庫存案例:如果第一次已經扣減,庫存端又把重送當成新要求,同一筆操作就可能扣兩次。
以前幾篇的地址修改來說,單看地址欄位,連續寫入相同內容,最後的值不會因此多一份。但如果更新還會觸發通知等動作,就要把那些影響一起確認。能不能重複執行,要看整個操作會做什麼。
庫存扣減則很直接:同一筆要求扣兩件,就只能產生一次扣兩件的效果。不過,這不表示請求絕對不能重送。呼叫逾時時,後端可能已經完成,只是回應沒有送達;我們需要的是讓同一筆要求再次抵達,也不會重複扣減。這就是冪等性。
防重,先讓系統認得同一筆操作
訂單端第一次送出扣減前,先為這筆操作建立並保存固定識別。之後因為逾時重送,就沿用同一個識別,讓庫存端知道這是原本那筆要求,而非另一筆新的扣減。
識別要能分清實際操作。一張訂單可能包含多個品項或多次合法異動,不能只看到訂單號碼相同,就把所有扣減都當成重複。每次呼叫都重新產生的追蹤編號,也無法辨認它們其實來自同一筆操作。
庫存端則保存這筆操作的內容與結果。再次收到相同識別時,先核對品項、數量等內容是否一致:第一次扣兩件,第二次卻要求扣三件,就不能當成相同要求。
若原操作已完成,就回傳原結果,不再扣減;若還在處理,就依約定等待或回覆處理中。只把重複要求略過,卻沒讓呼叫端知道結果,仍然沒有解決逾時後的不確定。
如果防重紀錄會定期清理,還要確認它的保存期間是否涵蓋允許重試的時間。否則,舊要求在紀錄刪除後再次送達,就可能被誤認為新操作。超過期限的要求要如何查明與處理,也需要事先約定。
因此要把「如何辨認同一筆操作」與「再次收到時回傳什麼」一起確認。上一篇的修改歷史用來追溯發生過什麼;這裡的紀錄還要參與執行判斷,有留下歷史不代表已經能防重。
有防重紀錄,還要考慮同時執行
接著想一個情況:同一筆要求幾乎同時送達,兩個處理程序都先查紀錄,也都看到「還沒處理」。如果各自繼續扣庫存,再補上紀錄,仍然可能扣兩次。
所以「先查過沒有」並不是保證。以防重紀錄與庫存都存在同一個資料庫為例,可以對操作識別設定唯一限制,讓同一識別只能建立一筆紀錄。兩個程序同時建立時,由資料庫處理衝突,不能讓兩邊都當成新操作繼續扣減。
防重紀錄、扣減與結果也要放在同一個交易裡,一起完成或一起回復。如果先扣了庫存,卻在留下紀錄前中斷,下次仍可能再扣;反過來,先記成完成,還沒扣就中斷,又會讓系統誤以為做完了。交易要避免的,就是這種只做一半的結果。
這裡有兩層保護:唯一限制避免同一筆操作重複建立,交易讓紀錄與扣減保持一致。另一個程序遇到衝突後,仍須依交易結果取得原操作狀態,不能直接當作成功。
不同訂單搶同一份庫存,則是另一個問題。例如庫存剩三件,兩張訂單各要兩件,兩者識別不同,還是要另外保護庫存不能超扣。防重處理的是同一筆操作重複執行,並不涵蓋所有併發問題。
加上重試之前,先確認重複執行的結果
在分析重試流程時,可以沿著三個問題檢查:重複執行會多做什麼?系統如何認出同一筆操作?兩次要求同時到達時,保護是否仍然成立?
驗證也要對應這些問題。讓庫存扣減完成後的回應遺失,再重送同一筆要求;也讓兩個相同要求同時送達。確認的不只是回傳成功,還要看庫存只扣一次,而且能取得正確的操作結果。
不是所有失敗都適合重試,也不是所有操作都需要同一套防重設計。先確認哪些效果允許重複、哪些只能發生一次,再決定如何重送,才能判斷AI提出的處理是否安全。
請求可以再來一次,庫存不能因此再扣一次。真正需要保證的,是同一筆操作無論送來幾次,都不會多做原本只該做一次的事。
「客戶說滑鼠連點兩下,同一班高鐵就訂了兩張票。這是Bug吧?」
「你看到的是Bug,我看到的是兩倍年終獎金。」
![]()