假設你在購物網站按下「付款」。

畫面轉了幾秒,最後跳出「連線逾時」。你不知道付款到底有沒有成功,所以又按了一次。

如果後端把兩次 request 都當成全新的付款指令,你可能就被扣了兩次款。

這就是 Idempotency 要處理的問題之一。

Idempotency 中文常翻成「冪等性」。它聽起來很數學,但在軟體系統裡其實可以先用一句話理解:

同一個操作重複執行多次,最後造成的系統狀態,和只執行一次相同。

重點不是「伺服器只能收到一次 request」,而是即使 request 被重送,系統也不應該產生不該重複的副作用。

Request 為什麼會重複?

很多人第一次設計 API 時,會假設:

Client 發一次 request,Server 收一次,處理一次,再回一次 response。

真實網路沒有這麼乾淨。

例如:

  1. Client 發出付款請求。
  2. Server 已經成功扣款。
  3. Server 準備回傳成功 response。
  4. 回程網路斷線。
  5. Client 沒收到 response,以為操作失敗。
  6. Client 自動 retry。
  7. 同一筆付款再次送到 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 不再建立新的付款,而是回傳第一次操作的結果。

概念上可以想成:

第一次:

key A → 執行付款 → 成功 → 記住結果

第二次:

key A → 發現處理過 → 不重新付款 → 回傳原結果

因此真正代表「這是不是同一個操作」的,不再只是 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 從危險動作,變成可以被系統安全接受的正常行為。

把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。