簡單易懂的冪等(附示例)
什麼是冪等性?
冪等性是分佈式系統和微服務中的一個關鍵概念,尤其是在處理重試和防止重複操作時非常重要。
簡單來說,冪等性意味着無論你發送相同的請求多少次,結果都是一樣的,系統不會執行重複的操作。
對於網絡失敗或超時等情況下非常重要,因爲客戶端可能會重試請求,但我們希望避免同一操作被重複執行。
冪等性的關鍵概念
1. 冪等操作
操作是冪等的,表示重複執行不會產生額外的影響。例如,用相同數據更新資源,或調用創建記錄的接口,不會導致重複記錄的生成。
2. 冪等鍵 (Idempotency Key)
在許多系統中,請求中包含一個唯一標識符(冪等鍵),以確保如果請求被重試,系統能夠識別爲重複操作並跳過執行。冪等鍵通常由客戶端生成。
示例場景
假設在電商系統中進行支付。你發送了一次支付請求,但由於網絡問題,客戶端不確定支付是否成功。
-
如果沒有冪等性,客戶端重試請求可能導致支付被重複處理。
-
如果有冪等性,客戶端在重試時使用相同的冪等鍵,服務器檢查到此請求已處理,會返回原始響應而不會重複執行操作。
實現冪等性的步驟
- 生成冪等鍵
客戶端爲每個請求生成唯一的冪等鍵(如 UUID),並將其包含在請求頭中。示例:
POST /payments
Content-Type: application/json
Idempotency-Key: abc123xyz
- 服務端存儲冪等鍵
服務端需要存儲每個冪等鍵及其對應的處理結果,可以使用 Redis、數據庫或其他持久化存儲。
- 檢查冪等鍵是否存在
-
如果冪等鍵已存在,服務器直接返回存儲的結果而不重複執行操作。
-
如果冪等鍵不存在,服務器執行操作並存儲該冪等鍵與響應結果。
- 設置冪等鍵過期時間
冪等鍵應設置合理的過期時間(如 24 小時),避免存儲無限增長。
用 go-zero 實現冪等性
下面以支付服務爲例,用 go-zero 實現冪等性功能。
示例代碼:冪等支付服務
package main
import (
"errors"
"fmt"
"net/http"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)
type PaymentRequest struct {
IdempotencyKey string `header:"Idempotency-Key"`
Amount float64 `json:"amount"`
}
type PaymentResponse struct {
Message string `json:"message"`
}
type PaymentHandler struct {
redisClient *redis.Redis
}
func NewPaymentHandler(redisClient *redis.Redis) *PaymentHandler {
return &PaymentHandler{redisClient: redisClient}
}
func (h *PaymentHandler) Handle(w http.ResponseWriter, r *http.Request) {
var req PaymentRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
// 檢查冪等鍵是否存在
val, err := h.redisClient.GetCtx(r.Context(), req.IdempotencyKey)
if err == nil && len(val) > 0 {
// 如果存在,返回之前的結果
httpx.OkJson(w, PaymentResponse{Message: val})
return
}
if err != nil {
logx.Errorf("redis error: %v", err)
httpx.Error(w, errors.New("internal server error"))
return
}
// 模擬支付處理邏輯
result := fmt.Sprintf("Payment Successful, amount: %.2f, transaction-id: %s",
req.Amount, stringx.RandId())
// 將冪等鍵及結果存入 Redis,設置過期時間爲 24 小時
err = h.redisClient.Setex(req.IdempotencyKey, result, 24*60*60)
if err != nil {
logx.Errorf("Failed to store idempotency key: %v", err)
httpx.Error(w, errors.New("internal server error"))
return
}
// 返回結果
httpx.OkJson(w, PaymentResponse{Message: result})
}
func main() {
// 初始化 Redis 客戶端
redisClient := redis.New("localhost:6379")
server := rest.MustNewServer(rest.RestConf{
Host: "localhost",
Port: 9999,
})
defer server.Stop()
// 註冊路由和支付處理邏輯
handler := NewPaymentHandler(redisClient)
server.AddRoute(rest.Route{
Method: http.MethodPost,
Path: "/payment",
Handler: handler.Handle,
})
server.Start()
}
代碼關鍵點
-
Redis 存儲:使用 go-zero 的 redis 客戶端存儲冪等鍵及其對應結果,避免重複執行。
-
冪等檢查:接收到請求時,先查詢 Redis 中是否存在冪等鍵,存在則直接返回結果。
-
過期時間:爲冪等鍵設置 24 小時過期時間,減少存儲開銷。
測試冪等性功能
1. 首次請求:
curl -X POST http://localhost:9999/payment \
-H "Content-Type: application/json" \
-H "Idempotency-Key: abc123xyz" \
-d '{"amount": 100}'
響應:
{"message":"Payment Successful, amount: 100.00, transaction-id: 1970cd852d278182"}
2. 重複請求:
curl -X POST http://localhost:9999/payment \
-H "Content-Type: application/json" \
-H "Idempotency-Key: abc123xyz" \
-d '{"amount": 100}'
響應:
{"message":"Payment Successful, amount: 100.00, transaction-id: 1970cd852d278182"}
3. 不帶冪等 key 的請求
curl -i -X POST http://localhost:9999/payment \
-H "Content-Type: application/json" \
-d '{"amount": 100}'
響應
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 19 Jan 2025 15:17:50 GMT
Content-Length: 35
field "Idempotency-Key" is not set
總結
冪等性是分佈式系統中處理網絡重試和防止重複執行操作的關鍵。
-
存儲冪等鍵:可以通過 Redis 或數據庫實現高效的冪等鍵存儲和查詢。
-
設置過期時間:確保存儲系統不會無限膨脹。
-
設計模式擴展:對於複雜場景可以結合分佈式事務和補償處理。
項目地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支持我們!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pZbiIvrtxGNSp7sJyc1Tqg