# 冪等性 Idempotence

**冪等性**(Idempotence) 是分散式系統和微服務架構中的一個關鍵概念, 尤其是在處理 retry 與 防止重複操作時非常重要. 簡單來說, 冪等性意味著無論你發送相同的命令多少次, 結果都是一樣的, 系統不會改變其狀態, 甚至可能不會執行重複的操作. 對於網路異常或請求超時等情況下非常重要, 因為 client 可能會 retry, client 屬於我們不可控的對象, 但我們服務方會希望避免同一操作被重複執行.

## 冪等性的關鍵概念
1. 冪等操作
該操作是**冪等**的, 表示重複執行不會產生額外的影響或狀態的改變. 例如, 使用相同的數據來更新資料的狀態, 或調用建立紀錄的端點, 這些都不會導致重複紀錄的產生.


2. 冪等欄位(Idempotency Key)
在許多系統的設計中, 請求或命令中都會包含一個唯一識別符(冪等欄位), 以確保如果請求被retry, 系統能夠識別這是重複操作, 可以選擇跳過執行. 冪等欄位通常是由client端產生.

> Retry Attack（重試攻擊） 是一種與網絡安全相關的攻擊方式，攻擊者利用系統允許的重試機制，通過多次嘗試重複提交請求，來實現以下目標：
> 
> - 繞過安全檢查：例如，發送多次支付請求，試圖重複扣款或重複使用一次性 token。
> - 猜測敏感數據：例如，不斷重試不同的憑證或密碼，試圖獲取有效的憑據。
> - 利用系統異常：利用系統在某些情況下的異常行為（如高併發導致系統容量耗盡或錯誤處理），達成攻擊目的。

### 冪等欄位的設計原則
- 唯一性：每個請求都應該有一個唯一的標識符，避免重複。
- 可追蹤性：冪等欄位應該能讓服務端快速定位相關請求, (很類似 [OpenTelemetry 中的 Trace Id](https://opentelemetry.io/docs/concepts/signals/traces/))。
- 安全性：避免使用敏感內容（如用戶名、密碼）作為冪等欄位。
- 可控性：通常由client端生成，因為服務端無法完全控制請求的重發行為

> 肯定有人會提出, 那讓client端請求前先跟server端獲取一個冪等值, 給client發出請求時攜帶著. retry時就重複使用該冪等值. 但client端的邏輯是我們不可控制的, 搞不好他每次retry都重新獲取一個冪等值 QQ

### 冪等欄位的組成
冪等欄位通常是一個唯一識別符（UUID 或類似的值），但可以根據業務需求進行擴展設計。以下是常見的組成方式：
1. 使用 UUID（Universally Unique Identifier）或 GUID（Globally Unique Identifier）。
```idempotency_key: "4f9e8c3a-9a6b-4d7a-8c3e-123456789abc"```

優點：生成簡單且不依賴具體業務邏輯。
缺點：無法反映請求的業務語義，僅適合通用場景。

2. 基於業務的唯一標識符
將業務相關的資訊組合起來，生成冪等欄位。例如：
```idempotency_key: "order_12345_user_67890"```

優點：冪等欄位與業務強相關，便於排查問題。
缺點：欄位設計需要根據具體業務場景定制, 不容易有統一標準.

3. 基於 Hash 的唯一標識符
將請求的核心參數內容（如用戶 ID、訂單 ID、請求時間等）拼接後產生 hash value 作為冪等欄位。
```idempotency_key: SHA256("user_67890_order_12345_20250124T095200")```

優點：減少冪等欄位的長度，並隱藏業務細節。
缺點：需要額外的hash計算，增加了一些處理成本。

4. 基於 Timestamp 的唯一標識符
在唯一標識符中加入時間戳，確保每次請求的冪等欄位不同。[Snowflake Id](https://ithelp.ithome.com.tw/articles/10236876) 和 [UUIDv7](https://uuid7.com/) 就基於這種概念.
```idempotency_key: "4f9e8c3a-9a6b-4d7a-8c3e-123456789abc_20250124T095200"```

優點：適合需要頻繁生成冪等欄位的場景。
缺點：client 端如果是使用發出請求時的 timestamp 來產生唯一標識符, 會影響重試機制的有效性。

> 數字或唯一標識符字串(UUID這類), 各有優缺點. 
> 數字的優點是好比較, payload size小; 缺點是極易被猜測出來範圍.
> 唯一值字串反過來, 但缺點大部分能用鈔能力解決.

## 應用場景
假設是在電商或金流系統中進行支付. 你發送了一次支付請求, 但因為網路問題, client 不確定支付是否成功.

- 如果沒有**冪等性**的設計, client 端 retry 請求可能導致支付被重複處理.
- 如果有**冪等性**的設計, client 端在retry之後, 服務方檢查到此請求已處理過, 通常會回傳原始的回應內容, 而選擇不會重複執行操作.

## 實現**冪等性**的步驟
1. Client 端產生冪等欄位與值
2. Server 端儲存冪等欄位與值
3. Server 端檢查冪等欄位與值是否存在
  - 如果已經存在, 回傳儲存的結果而不重複執行操作
  - 不存在, 則執行操作並儲存冪等欄位與值, 應設置冪等欄位的 Expiration, 避免儲存列表無限增長

## 冪等欄位, Nullifier, Nonce
當我們談到**冪等欄位**會發現還有兩個術語的改念與這很向, 分別是**Nullifier** 和 **Nonce** 時，這三個名詞都與分散式系統、區塊鏈和密碼學中的「唯一性」和「防止重複操作」的需求密切相關。雖然它們的具體用途和應用領域不同，但核心概念有一定的相似性。以下是對這三個名詞的統一說明及比較。

Nullifier 和 Nonce 可以被視為冪等欄位的一種實現形式，只是它們的應用領域和具體功能略有不同。
**統一的核心概念**
這三個名詞的共同點在於：

**唯一性**：每個值都必須是唯一的，避免重複操作或數據衝突。
**一次性使用**：一旦被使用，就不能再次重複使用，從而保證系統的正確性和安全性。
**防止重複執行或攻擊**：這三者的設計目的是防止因重複執行請求、交易或操作而導致的錯誤或攻擊。

### 三者的具體定義與應用場景
#### **冪等欄位（Idempotency Key）**
定義：冪等欄位是一個唯一的標識符，用於標記一次操作（如 API 請求）是否已經執行過，從而保證即使重複執行相同的操作，系統的狀態也不會改變。

應用場景：
分散式系統：當客戶端因網路超時或錯誤而重發請求時，服務端可以通過檢查冪等欄位來確保操作不會被重複執行。
支付系統：避免同一支付請求重複扣款。

特點：
通常由客戶端生成，並附加到請求中。
服務端需要記錄已使用的冪等欄位，並檢查其唯一性。

示例：
```json
{
  "idempotency_key": "123e4567-e89b-12d3-a456-426614174000",
  "action": "create_order",
  "data": { ... }
}
```

#### **Nullifier**
定義：Nullifier 是一個唯一的標識符，用於標記某個資源或交易是否已經被使用過，特別是在隱私保護協議中，用於防止雙花（Double-Spending）或重複使用。

> 雙花（Double-Spending） 是區塊鏈和加密貨幣領域中的一個關鍵問題，指的是同一筆加密貨幣被用戶多次花費的情況。這種情況如果發生，會破壞加密貨幣系統的完整性和信任基礎。

應用場景：
隱私幣（如 Zcash）：用於標記某筆資金是否已經被花費。
匿名交易協議（如 Tornado Cash）：確保用戶提現後，不能再次使用相同的憑證進行提現。

特點：
通常由服務端或協議生成，基於某些唯一的資源（如交易 ID 或用戶密鑰）計算得出。
與隱私保護密切相關，Nullifier 本身不暴露具體資源內容。

示例：
```
Nullifier = Hash(User_Private_Key || Resource_ID)
```
#### **Nonce**
定義：Nonce 是一個用一次的數字或唯一值，用於加密協議、身份驗證或區塊鏈中，確保操作的唯一性或防止重放攻擊。[JWT RFC 7800](https://datatracker.ietf.org/doc/html/rfc7800#section-3.6) 中也有使用到Nonce。

應用場景：
區塊鏈挖礦：礦工調整 Nonce 值，直到找到滿足特定哈希條件的區塊。
交易順序：以太坊中的交易 Nonce 用於標記交易的順序，防止交易亂序或重複執行。
防重放攻擊：在 API 調用或身份驗證中，Nonce 確保每次請求都是唯一的。

特點：
可以是隨機生成的數字，也可以是遞增的整數（根據具體應用場景）。
一旦使用，便失效，不能重複使用。

示例：
區塊鏈挖礦中的 Nonce：
```
Nonce = 42
```
API 請求中的 Nonce：
```
{
  "user_id": "12345",
  "action": "login",
  "nonce": "a1b2c3d4e5"
}
```

## 分散式事務與補償處理
在分散式系統中，操作可能涉及多個服務或資料庫，難以保證所有操作都在同一事務中完成。因此，經常使用以下設計模式來處理一致性問題：

 **分散式事務**：通過 2PC或 3PC 來保證多個節點的一致性，但這種方式通常會降低性能，並且在高併發場景下不一定實用。
 
**補償處理**（Compensating Transactions）：當某些操作失敗時，通過執行反向操作（補償操作）來撤銷已完成的部分。例如，若訂單支付成功但扣庫存失敗，則需要補償支付操作（退款）。

> 參考[微服務瞎談(7) Saga Pattern](https://ithelp.ithome.com.tw/articles/10236124)

###  冪等性與分散式事務/補償處理的關聯
在分散式系統中，冪等性和補償處理經常結合使用，因為它們可以共同解決以下問題：

（1）重試機制中的冪等性
在分布式事務中，網絡波動可能導致某些操作需要重試。如果操作具有冪等性，多次執行不會對系統狀態造成影響，這樣可以簡化重試邏輯，避免數據錯亂。

例如：

在支付服務中，當支付請求因網絡超時被重試時，冪等性可以保證多次支付請求只會成功一次。

（2）補償操作的冪等性
補償操作本身也需要冪等性，因為補償可能因網絡問題或重試機制被多次執行。如果補償操作不是冪等的，可能會導致系統狀態不一致。

例如：

若退款操作被多次執行，可能導致多次退款給用戶，這是不可接受的。通過冪等性，系統可以保證只退款一次。

（3）分布式事務的簡化
在一些場景下，冪等性可以減少對分散式事務的依賴。例如，當操作本身具有冪等性時，系統可以允許某些操作失敗後重試，而不必執行複雜的分散式事務協調。

### 實現冪等性的常見方法
在設計分散式系統時，為了實現冪等性，通常會採用以下方法：

- 冪等欄位：為每個請求生成冪等欄位，服務端根據冪等欄位判斷請求是否已處理過。
- 樂觀鎖：通過版本號控制更新操作，避免多次執行導致數據錯亂。
- 狀態機檢查：在執行操作前檢查當前狀態，確保操作是否符合預期。
- 資料去重複：在資料層面設計去重機制，避免重複執行對數據造成影響。


## 總結
**冪等性**是分散式系統中處理網路retry 與防止重複執行操作的關鍵. 對於複雜場景可以結合分散式事務設計與補償處理.

