冪等任務設計指的是:確保一個任務不管被執行一次還是重複執行好幾次,最終產生的結果都完全一樣,不會因為多跑了幾次而累積出額外的、不該有的後果。這個概念直接支撐著 retry policy 的安全性——重試策略假設「再跑一次」是安全的動作,但這個假設只有在任務本身冪等的情況下才成立。如果任務不冪等(例如「幫這筆訂單加 100 元紅利」這種每次執行都會疊加效果的動作),重試就不是在修正問題,是在製造新問題:任務因為網路問題失敗了,你以為沒執行成功而重試,結果第一次其實已經寫入成功,兩次疊加後紅利多加了一倍,這個錯誤比原本的失敗更難發現,因為系統看起來一切正常,只是數字不對。
這個問題會被需要,是因為重試策略跟冪等設計解決的是失敗處理裡的兩個不同層面,但經常被誤以為只要有前者就夠了。重試策略回答「失敗後要不要再試」,冪等設計回答「再試這個動作本身安不安全」——如果沒有先確認任務冪等,重試策略的存在反而會放大風險:原本沒有重試機制時,失敗頂多是任務沒完成;有了重試機制卻沒有冪等設計,失敗加上重試的組合,可能讓一個原本單純的失敗,變成一個疊加了兩次甚至三次副作用的錯誤。這正是為什麼冪等設計要在設計重試策略之前先確認,而不是兩者獨立處理,順序顛倒會讓原本用來提升可靠度的機制,反過來變成放大錯誤的機制。
實務上有一個核心判斷方法:問這個任務的每一步「如果這個動作被執行兩次,結果會不會跟執行一次不一樣」。常見的非冪等動作包括:用「加總」邏輯累計數字(每次執行都多加一次)、用「新增一筆」邏輯寫入資料(重複執行會產生重複紀錄)、發送通知或郵件(重複執行等於重複打擾收件人)。要把這些動作改成冪等,常見做法是把「加總」改成「設定為某個絕對值」(例如不寫「餘額加 100」,改寫「餘額設為原始餘額加 100 後的最終數字,並記錄這筆交易的唯一識別碼」);把「新增一筆」改成「先檢查是否已存在同樣識別碼的紀錄,存在就跳過、不存在才新增」;發送通知類的動作則要有明確的「已發送」標記,重試前先檢查標記,標記存在就不再發送。核心思路都是:讓任務執行前先檢查「這件事是不是已經做過了」,而不是每次都當成全新的動作執行。
對你來說,冪等任務設計真正的價值在於:讓 retry policy 這個原本用來提升可靠度的機制,真正發揮它該有的作用,而不是反過來變成放大錯誤的來源。任何你在設計排程任務時打算搭配重試策略的動作,動筆前都值得先問一句「這個動作重複做兩次,會不會跟做一次不一樣」,如果答案是會,代表這個任務目前還不安全,不能直接套用重試策略,需要先做冪等化的修改。要留意的是:冪等化的修改往往需要額外的狀態記錄(例如前面提到的唯一識別碼、已發送標記),這些記錄本身也要正確地被儲存跟查詢,如果記錄機制本身不可靠,冪等設計會失去意義——確認任務有沒有做過的那個檢查機制,本身也不能是會失敗的環節。
Stripe 這類支付服務的官方 API 文件裡,把「幂等鍵」(idempotency key)列為建立扣款請求時的建議做法,說明使用者可以在每次請求裡附上一個唯一識別碼,如果因為網路問題重複送出同一個請求,伺服器會辨識出這個識別碼已經處理過,直接回傳原本的結果而不會重複扣款;這個機制存在的理由,正是因為支付這類動作一旦重複執行,後果(重複扣款)遠比單純的請求失敗更難處理。
優點是讓重試策略真正安全可用,即使任務因為各種原因被重複執行,結果也不會累積出額外的錯誤;缺點是冪等化的修改需要額外的設計成本,通常要引入唯一識別碼或狀態標記等機制,這些機制本身也要正確實作跟儲存,如果沒做好,冪等設計反而會增加系統複雜度卻沒有真正達到保護效果。