按下付款按鈕後,畫面卻卡了好一陣子,難免讓人不安。你會想再按一次。
但是按兩次,不就會付款兩次嗎?
設計良好的系統不會。能保證這件事的特性有一個名稱。
就是冪等性。
這個概念會出現在後端文件、HTTP規格和付款API指南中,但定義本身一行就能說完。真正困難的是「為什麼它如此重要」以及「要如何實作」。
本文就來補上這兩個答案。
先看重點整理。
- 冪等性:相同要求送出一次或多次,結果都相同的特性
- 重要原因:網路會失敗,失敗就需要重試,而要讓重試安全,操作就必須具備冪等性
- 在HTTP中,GET・PUT・DELETE具冪等性,POST則不具冪等性
- 讓POST安全的實務機制是Idempotency-Key
定義:做很多次,等同於做一次
冪等性一詞源自數學。若某個運算f滿足f(f(x)) = f(x),就稱為冪等。
絕對值函數就是很好的例子。 |−5| = 5,而且 |5| = 5。
套用一次或一百次,結果都相同。
套用到API上,就是這樣。
相同要求送出1次或N次,伺服器狀態都相同。
注意:不代表回應必須相同,而是伺服器狀態必須相同。即使第二次DELETE要求回傳404,只要「該資源不存在」的伺服器狀態相同,就仍具冪等性。
日常例子是電梯按鈕。即使按五次5樓按鈕,電梯也只會前往5樓一次。
反過來,如果是「再上升5層」按鈕,每按一次結果都會不同。前者具冪等性,後者不具冪等性。
為什麼重要:網路一定會失敗
冪等性為什麼重要,用一個情境就能說明。
用戶端送出付款要求,伺服器完成付款處理。
但回應傳回途中,網路中斷了。
用戶端無從得知:要求是在抵達伺服器前失敗(未付款),還是處理完成後只有回應遺失(已付款)。
因為兩種情況看起來都一樣,都是逾時。
選擇只有兩個:放棄重試(付款可能未完成),或進行重試(若已完成就會重複付款)。兩個都很糟。
冪等性消除了這個兩難。如果要求具冪等性,「不確定就再送一次」永遠是安全策略。
自動重試、訊息佇列的至少一次傳遞(at-least-once),以及用戶端不斷重新整理,都不再需要害怕。
這就是為什麼每份分散式系統設計文件都反覆寫著:「只對具冪等性的操作設定重試。」
從HTTP方法看冪等性
HTTP規格(RFC 9110)明確定義了各方法的冪等性。
| 方法 | 具冪等性? | 原因 |
|---|---|---|
| GET | O | 查詢不會改變狀態 |
| PUT | O | 「替換成這個值」做幾次都是相同的值 |
| DELETE | O | 「刪除它」做幾次都是已刪除狀態 |
| POST | X | 「建立新的」每呼叫一次就增加一個 |
| PATCH | 有條件 | 「將此欄位設為X」具冪等性,「加1」不具冪等性 |
PUT與POST的對比是關鍵。PUT指定「結果狀態」,因此具冪等性;POST指示「行為」,因此不具冪等性。
PATCH則取決於內容。
瀏覽器按上一頁回到POST頁面時會警告「要重新提交表單嗎?」,也是因為瀏覽器知道重新送出POST並不安全。
實務機制:冪等性金鑰
然而,註冊會員、建立訂單和付款本質上都是POST。無法完全避免不具冪等性的工作。
因此出現了讓不具冪等性的要求具備冪等性的機制:Idempotency-Key。
運作方式很簡單。
- 用戶端為每個要求產生唯一金鑰(通常是UUID),並放入標頭送出
- 伺服器第一次看到該金鑰時正常處理,並將回應與金鑰一併儲存
- 再次收到相同金鑰時不再處理,直接回傳已儲存的回應
無論是重試還是連按兩次,只要使用相同金鑰,伺服器就只處理一次。Stripe與Toss Payments等付款API實際支援這個標頭,付款整合指南也將它列為必備項目。
用戶端有一個細節要注意:「重試時要使用相同金鑰」。
如果每次重試都產生新金鑰,伺服器就會把它視為不同要求。金鑰應以「相同意圖的要求」為單位建立。
總結
- 冪等性是指無論相同要求送出幾次,伺服器狀態都相同的特性(f(f(x)) = f(x))
- 網路失敗造成「不知道是否完成」時,只有具備冪等性,重試才會安全
- HTTP:GET・PUT・DELETE具冪等性,POST不具冪等性,PATCH取決於內容
- PUT指定結果狀態,因此具冪等性;POST指示行為,因此不具冪等性
- 使用Idempotency-Key讓不具冪等性的POST具備冪等性——相同金鑰只處理一次
- 重試時務必重用相同金鑰。金鑰以「相同意圖的要求」為單位

