假設你在購物網站按下「付款」。
畫面轉了幾秒,最後跳出「連線逾時」。你不知道付款到底有沒有成功,所以又按了一次。
如果後端把兩次 request 都當成全新的付款指令,你可能就被扣了兩次款。
這就是 Idempotency 要處理的問題之一。
Idempotency 中文常翻成「冪等性」。它聽起來很數學,但在軟體系統裡其實可以先用一句話理解:
同一個操作重複執行多次,最後造成的系統狀態,和只執行一次相同。
重點不是「伺服器只能收到一次 request」,而是即使 request 被重送,系統也不應該產生不該重複的副作用。
Request 為什麼會重複?
很多人第一次設計 API 時,會假設:
Client 發一次 request,Server 收一次,處理一次,再回一次 response。
真實網路沒有這麼乾淨。
例如:
- Client 發出付款請求。
- Server 已經成功扣款。
- Server 準備回傳成功 response。
- 回程網路斷線。
- Client 沒收到 response,以為操作失敗。
- Client 自動 retry。
- 同一筆付款再次送到 Server。
這時 Server 看到的,是第二個合法的 HTTP request。
但對使用者來說,這仍然只是「同一次付款」。
問題因此變成:
系統怎麼知道第二個 request 是一次新的操作,還是前一次操作的重試?
冪等不是「只能執行一次」
這裡最容易誤會。
Idempotency 不代表 Server 永遠只執行一次程式碼。
同一個 request 可能真的被傳送很多次,API handler 也可能真的被呼叫很多次。
重要的是最終效果不能一直累加。
例如把使用者名稱設定成:
name = "Norelyn"
執行一次之後是 Norelyn。
再執行十次,仍然是 Norelyn。
這種操作天然就比較接近 idempotent。
但如果操作是:
balance = balance - 100
那每執行一次,餘額都會再少 100。
這就不是 idempotent。
HTTP Method 本身也有冪等概念
HTTP 規範裡,一些 method 被定義為 idempotent。
常見例子包括:
GET
PUT
DELETE
而 POST 通常不被視為天然 idempotent。
例如:
GET /users/42
不管查詢幾次,都不應該因為「查詢」本身把資料改掉。
PUT /users/42
如果內容是:
{
"name": "Norelyn"
}
重送多次,理論上最後仍然只是把 name 設成 Norelyn。
DELETE /users/42
第一次刪除之後,後面再送 DELETE,資源仍然維持「不存在」的狀態。
相反地:
POST /orders
通常代表「建立一筆新訂單」。
如果完全相同的 POST 被送兩次,Server 很可能會建立兩筆訂單。
所以「HTTP method 是否 idempotent」與「你的業務操作會不會產生重複副作用」必須一起考慮。
最典型的做法:Idempotency-Key
對付款、建立訂單、建立任務這類不能隨便重複的操作,常見做法是讓 Client 產生一個唯一識別碼。
例如:
Idempotency-Key: 8ec37f6d-...
第一次 request 抵達時,Server 會記錄:
這個 key 對應到哪個操作、結果是什麼。
如果之後收到相同 key,Server 不再建立新的付款,而是回傳第一次操作的結果。
概念上可以想成:
第一次:
第二次:
因此真正代表「這是不是同一個操作」的,不再只是 request body,而是 Idempotency-Key。
為什麼不能只比較 request body?
假設兩位使用者都買同一個 100 元商品。
兩次 request body 可能完全一樣:
{
"product_id": 123,
"amount": 100
}
但這可能是兩筆不同的合法訂單。
反過來,同一個付款 retry 時,request body 也可能因 timestamp 或其他欄位而略有不同。
所以單純比較 body 很容易誤判。
Idempotency-Key 的用途,就是提供一個由 Client 明確指定的「這是哪一次操作」識別碼。
Key 要存多久?
Server 不可能永遠保存所有 Idempotency-Key。
因此實作時通常會設定有效期限,例如:
24 小時
48 小時
7 天
實際時間取決於系統的 retry 行為與業務需求。
保存的內容通常至少包含:
key
操作類型
處理狀態
response
建立時間
有些系統還會保存 request 的摘要或 hash。
這樣如果 Client 用同一個 key 卻送來完全不同的內容,Server 可以拒絕,而不是誤把不同操作當成同一件事。
如果第一次 request 還沒做完呢?
另一個麻煩情況是:
第一個 request 還在處理時,第二個相同 key 已經進來。
假設兩個 request 同時查資料庫:
「這個 key 存在嗎?」
兩邊都看到「不存在」。
如果接著兩邊都執行付款,仍然可能重複扣款。
所以真正可靠的 Idempotency 實作通常還需要:
unique constraint
transaction
atomic insert
lock
例如資料庫可以規定:
idempotency_key 必須 UNIQUE。
兩個 request 同時插入時,只會有一個成功取得這個 key 的處理權。
這也是為什麼 Idempotency 不只是「加一個 HTTP header」這麼簡單。
真正的安全性最後通常還是要落到資料庫的一致性機制。
Retry 與 Idempotency 為什麼常一起出現?
分散式系統裡,retry 幾乎不可避免。
API Gateway、SDK、Queue consumer、背景工作,甚至瀏覽器本身,都可能重新送出操作。
沒有 retry,短暫網路錯誤會讓系統很脆弱。
但有 retry,又可能讓同一個操作執行多次。
因此常見的設計其實是一組:
Retry + Idempotency
Retry 負責提高成功率。
Idempotency 負責確保 retry 不會製造重複副作用。
兩者不是互相替代,而是互相配合。
Message Queue 也會遇到一樣的問題
這個概念不只存在 HTTP API。
很多 Message Queue 採用 at-least-once delivery。
意思是:
系統盡量保證訊息至少會送到一次,但同一則訊息有可能被送到兩次以上。
因此 consumer 不能假設:
「我收到這個 message,就代表它一定從來沒被處理過。」
常見做法是替每個 message 帶一個 event ID,並在資料庫記錄哪些 event 已處理。
收到重複 event 時直接跳過。
這其實就是另一種 idempotent consumer。
Idempotency 不能解決所有一致性問題
即使有 Idempotency-Key,也不代表所有問題自動消失。
例如一個訂單流程可能包含:
扣款
寫入訂單
扣庫存
寄 Email
如果扣款成功後程式 crash,後面的動作可能沒有完成。
這時需要的可能還包括:
Database Transaction
Outbox Pattern
Saga
Message Queue
補償操作
Idempotency 解決的是「同一操作重複執行」這個問題。
它不是完整的分散式交易方案。
常見錯誤:看到 POST 就認為一定不能 retry
POST 並不代表永遠不能重試。
正確問題應該是:
這個 endpoint 有沒有設計重複請求的處理方式?
一個 POST /payments,如果支援 Idempotency-Key,就可以安全地對相同操作 retry。
相反地,即使某個 API 使用 PUT,如果後端實作偷偷做了:
send_email()
increment_counter()
那它仍然可能產生重複副作用。
HTTP method 提供的是語意約定。
真正能不能安全重試,最後仍取決於 Server 的實作。
一個簡化的付款流程
可以把可靠付款 API 想成:
Client
產生 operation ID
POST /payments
Idempotency-Key: abc123
Server 嘗試建立 key=abc123 的紀錄
若不存在:
若已存在且完成:
直接回傳原本結果
若正在處理:
等待、回傳 processing,或要求稍後重試
這樣即使 Client 因為 timeout 重送五次,系統仍然只會建立一筆付款。
什麼情況特別需要 Idempotency?
只要一個操作具有「不能重複發生」的副作用,就值得考慮。
例如:
付款
退款
建立訂單
發放點數
寄送一次性優惠券
建立雲端資源
提交表單
建立背景工作
處理 Queue event
相反地,純查詢通常不需要額外的 Idempotency-Key,因為它本來就不應該修改狀態。
最後記住一件事
網路系統不能保證一個 request 永遠只出現一次。
逾時不代表失敗,沒有收到 response 也不代表 Server 沒有執行。
因此可靠系統真正要設計的不是:
「怎麼確保 Client 永遠只送一次?」
而是:
「就算同一個操作被送了很多次,我的系統還能不能只產生一次正確的效果?」
這就是 Idempotency 最重要的價值。
它讓 retry 從危險動作,變成可以被系統安全接受的正常行為。
把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。