API冪等性設計全攻略:冪等键、分散式去重与支付安全介面

后端开发

摘要

  • 冪等性是分散式系統的「安全带」:網路重試、訊息重複、使用者双击,都可能導致介面被多次呼叫
  • 冪等键(Idempotency Key)是HTTP API冪等的標準做法,Stripe、支付宝、微信支付均已採用
  • 實作冪等有3层防線:客戶端冪等键 → 服務端去重表 → 資料庫唯一約束
  • 支付、下单、庫存扣减是冪等性要求最高的3类介面,面試必考
  • 本文提供从原理到Go實作的完整方案,含Redis + PostgreSQL双保险架構

目錄


什麼是冪等性,為什麼必須做

一句话定义

同一个請求无论執行多少次,结果都和執行一次相同,且不會有多余的副作用。

数学表达:f(f(x)) = f(x)

為什麼分散式系統一定会有重複請求

这不是「如果」的問題,而是「什么时候」的問題:

重複来源 典型場景 發生概率
使用者重複点击 支付按钮、提交訂單 每天都会遇到
網路逾時重試 客戶端自動重試、网关重試 生產环境常态
訊息佇列重複投遞 Kafka at-least-once、RabbitMQ重投 設計如此
負載均衡重試 Nginx/Envoy上游逾時重試 設定不当时频繁
微服務呼叫链重試 下游逾時,上游重发 鏈路越长越常见

不做冪等的真實代價

2025年某二手交易平台爆出「重複扣款」事故:使用者点击支付后網路抖动,客戶端自動重試3次,服務端没有冪等校验,導致同一訂單扣款3次。涉及金额超過200万元,監管部门介入調查。

冪等性不是優化项,是支付/交易类系統的准入门槛。


冪等性的4种等级

Level 0:天然冪等(HTTP GET/PUT/DELETE)

GET  /api/users/123        → 查詢,天然冪等
PUT  /api/users/123        → 全量更新,天然冪等
DELETE /api/users/123      → 刪除,天然冪等
POST /api/orders           → 建立,NOT冪等!

POST和PATCH預設不冪等,因为每次呼叫可能建立新資源或产生新副作用。

Level 1:業務层冪等

透過業務邏輯判斷:「这个訂單已经建立过了,直接返回之前的结果」。

func CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    existing, err := db.GetOrderByClientOrderId(ctx, req.ClientOrderId)
    if err == nil {
        return existing, nil  // 已存在,直接返回
    }
    return db.CreateOrder(ctx, req)
}

優點:簡單直接。缺點:並發时兩個請求可能同時透過檢查。

Level 2:冪等键 + 去重表

客戶端传唯一冪等键,服務端用去重表記錄「这个键是否已處理过」。

Level 3:資料庫唯一約束(最终防線)

在資料庫层加唯一索引,即使應用層有漏洞,資料庫也会拒绝重複插入。

CREATE UNIQUE INDEX idx_orders_idempotency_key
ON orders (idempotency_key)
WHERE idempotency_key IS NOT NULL;

生產推薦:Level 2 + Level 3 組合使用。


冪等键設計規範

冪等键的產生規則

規則 說明 範例
客戶端產生 由呼叫方在請求前產生UUID 550e8400-e29b-41d4-a716-446655440000
唯一性 同一業務操作使用同一个键 重試时必須複用原键
作用域 通常绑定 user_id + 操作類型 不同使用者可以用相同格式的键
有效期 24-72小时 過期后可複用(极少發生)
傳遞方式 HTTP Header Idempotency-Key: <uuid>

Stripe的冪等键規範(行業標竿)

POST /v1/charges
Idempotency-Key: uuid-v4-string
Content-Type: application/json

{"amount": 2000, "currency": "usd", "source": "tok_visa"}

Stripe的處理邏輯:

  1. 收到請求,檢查冪等键是否已存在
  2. 不存在 → 執行業务邏輯,儲存结果,返回201
  3. 已存在且參數相同 → 返回快取的结果(200)
  4. 已存在但參數不同 → 返回409 Conflict

冪等键 vs 請求ID vs 鏈路追蹤ID

欄位 用途 生命週期 是否用於去重
Idempotency-Key 業務冪等 24-72h
X-Request-ID 請求追蹤 單次請求
Trace-ID 分散式鏈路追蹤 單次呼叫链

三者不要混用。Trace-ID每次請求都不同,不能用於冪等。


3层冪等防線架構

客戶端                    服務端                              資料庫
  │                         │                                  │
  │  Idempotency-Key        │                                  │
  ├────────────────────────→│                                  │
  │                         │  Layer 1: Redis 快速去重          │
  │                         │  SETNX idem:{key} → 成功/失敗     │
  │                         │       │                           │
  │                         │       ▼                           │
  │                         │  Layer 2: 冪等記錄表               │
  │                         │  INSERT idempotency_records       │
  │                         │       │                           │
  │                         │       ▼                           │
  │                         │  Layer 3: 業務表唯一約束           │
  │                         │  INSERT orders (UNIQUE key)  ────→│
  │                         │                                  │
  │  ←── 返回结果 ──────────│                                  │

Layer 1:Redis SETNX 快速攔截

func (s *IdempotencyService) TryAcquire(ctx context.Context, key string, ttl time.Duration) (bool, error) {
    redisKey := fmt.Sprintf("idem:%s", key)
    acquired, err := s.redis.SetNX(ctx, redisKey, "processing", ttl).Result()
    if err != nil {
        return false, fmt.Errorf("redis setnx: %w", err)
    }
    return acquired, nil
}
  • SETNX 成功 → 首次請求,繼續處理
  • SETNX 失敗 → 重複請求,查快取结果返回
  • TTL 设为 24-72h,防止Redis記憶體洩漏

Layer 2:冪等記錄表

CREATE TABLE idempotency_records (
    id              BIGSERIAL PRIMARY KEY,
    idempotency_key VARCHAR(64) NOT NULL,
    request_hash    VARCHAR(64) NOT NULL,  -- 請求体雜湊,防參數篡改
    response_status INT,
    response_body   JSONB,
    status          VARCHAR(20) NOT NULL DEFAULT 'processing',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,

    CONSTRAINT uq_idempotency_key UNIQUE (idempotency_key)
);

CREATE INDEX idx_idem_expires ON idempotency_records (expires_at);

狀態流轉:processingcompleted / failed

Layer 3:業務表唯一約束

ALTER TABLE orders ADD COLUMN idempotency_key VARCHAR(64);
CREATE UNIQUE INDEX idx_orders_idem ON orders (idempotency_key);

即使Layer 1和Layer 2都失效(Redis當機、应用bug),資料庫唯一約束是最后防線。


Go生產级冪等中介軟體實作

冪等中介軟體(Gin框架)

type IdempotencyMiddleware struct {
    redis *redis.Client
    db    *sql.DB
    ttl   time.Duration
}

func (m *IdempotencyMiddleware) Handle() gin.HandlerFunc {
    return func(c *gin.Context) {
        key := c.GetHeader("Idempotency-Key")
        if key == "" {
            c.Next()
            return
        }

        ctx := c.Request.Context()

        cached, err := m.getCachedResponse(ctx, key)
        if err == nil && cached != nil {
            c.JSON(cached.Status, cached.Body)
            c.Abort()
            return
        }

        acquired, err := m.redis.SetNX(ctx, "idem:"+key, "1", m.ttl).Result()
        if err != nil {
            c.JSON(500, gin.H{"error": "idempotency check failed"})
            c.Abort()
            return
        }
        if !acquired {
            for i := 0; i < 30; i++ {
                time.Sleep(100 * time.Millisecond)
                cached, err = m.getCachedResponse(ctx, key)
                if cached != nil {
                    c.JSON(cached.Status, cached.Body)
                    c.Abort()
                    return
                }
            }
            c.JSON(409, gin.H{"error": "duplicate request in progress"})
            c.Abort()
            return
        }

        blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
        c.Writer = blw
        c.Next()

        m.saveResponse(ctx, key, c.Writer.Status(), blw.body.String())
    }
}

並發安全的關鍵點

兩個相同冪等键的請求同時到达时:

  1. Redis SETNX 只有一個能成功
  2. 失敗者進入輪詢等待(100ms × 30次 = 3秒)
  3. 成功者處理完毕后将结果寫入DB
  4. 失敗者从DB讀到快取结果返回

这个「SETNX + 輪詢等待」模式是Stripe、支付宝等大廠的標準做法。


支付場景冪等實戰

支付介面的完整冪等流程

使用者点击支付
    │
    ▼
客戶端產生 Idempotency-Key (UUID)
    │
    ▼
POST /api/payments {amount, order_id}
Header: Idempotency-Key: uuid
    │
    ▼
┌─ 冪等檢查 ─────────────────────────────┐
│ 1. Redis SETNX                          │
│ 2. 查冪等記錄表                          │
│ 3. 查訂單狀態(已支付?直接返回)          │
└─────────────────────────────────────────┘
    │ 首次請求
    ▼
┌─ 業務處理(資料庫交易)─────────────────┐
│ BEGIN                                   │
│   INSERT payment (status=pending)       │
│   呼叫支付渠道API                         │
│   UPDATE payment (status=success)       │
│   UPDATE order (status=paid)            │
│ COMMIT                                  │
└─────────────────────────────────────────┘
    │
    ▼
快取響應到冪等記錄表 + Redis
    │
    ▼
返回支付结果给客戶端

支付冪等的3个特殊考量

1. 支付渠道也要冪等

呼叫微信/支付宝时,必須传商戶訂單号(out_trade_no),同一訂單号渠道侧也会去重。

2. 狀態机保證

pending → processing → success
                    → failed
                    → timeout → (人工介入)

已 success 的訂單,任何重試都直接返回成功,不會再次扣款。

3. 對帳兜底

每日對帳任务檢查:支付渠道账单 vs 本地訂單,發現不一致时告警。冪等不是100%可靠的,對帳是最后的安全網。


面試高頻考點与踩坑指南

面試常問的6个問題

Q1:GET是冪等的,POST為什麼不是?

GET是读操作,不改變伺服器狀態。POST是写操作,每次呼叫可能建立新資源。但可以透過冪等键让POST具備冪等性。

Q2:Redis當機了冪等还有效吗?

Layer 1(Redis)失效后,Layer 2(冪等記錄表)和Layer 3(唯一約束)仍然生效。Redis是效能優化层,不是唯一保障。

Q3:冪等键過期了,使用者又发了一样的請求怎麼办?

過期后視為新請求。但業務层应檢查訂單狀態:如果訂單已支付,即使冪等键過期也直接返回已支付结果。冪等键是技術手段,業務狀態是最终判斷依據。

Q4:分散式锁能實作冪等吗?

可以,但不是最優方案。分散式锁解決的是並發互斥,冪等解決的是重複請求。锁的粒度、逾時、死鎖問題比冪等键複雜得多。推薦冪等键 + 去重表,而非分散式锁。

Q5:訊息佇列消費端怎麼做冪等?

三種方式:(1) 業務唯一键去重(推薦);(2) 消費端維護已處理訊息ID表;(3) 資料庫唯一約束。Kafka的冪等生產者是生產者侧保障,消費端仍需自行去重。

Q6:冪等和交易什么關係?

冪等保證「多次執行 = 一次執行」,交易保證「多個操作要么全成功要么全失敗」。支付場景两者都需要:交易保證扣款和更新訂單狀態的原子性,冪等保證重試不會重複扣款。

踩坑清單

表現 解法
冪等键由服務端產生 客戶端重試时鍵不同,去重失效 必須由客戶端產生并複用
只存成功结果 失敗重試时返回舊錯誤 成功和失敗结果都快取
不设過期时间 Redis/DB数据無限增長 TTL 24-72h + 定時清理
並發不加鎖 兩個請求同時透過檢查 SETNX + 輪詢等待
忽略支付渠道冪等 本地冪等了但渠道扣了兩次 商戶訂單号全域唯一

庫存扣減場景的冪等設計

支付冪等之外,庫存扣減是面試和實戰中的第二大高頻場景。推薦方案:冪等键 + 條件更新WHERE quantity >= $1),資料庫層原子守衛防止超賣。

分散式環境下的冪等挑戰

多實例並發用 SETNX + 輪詢等待;Redis 叢集故障時資料庫唯一約束是最終保障。

效能優化與企業落地案例

三層效能:純DB → Redis+DB → Bloom Filter+Redis+DB。某支付平台改造後連續8個月零重複扣款。

動手實戰與2026趨勢

30分鐘搭建冪等支付介面。趨勢:網關層標準化、OpenTelemetry整合、AI Agent API層冪等。


總結与延伸閱讀

API冪等性是后端開發的基本功,也是面試高頻考點。記住核心公式:客戶端冪等键 + Redis快速去重 + 資料庫唯一約束 = 生產级冪等

設計要點回顧

  1. POST介面預設不冪等,必須顯式設計
  2. 冪等键由客戶端產生,重試时複用同一个键
  3. 3层防線:Redis SETNX → 冪等記錄表 → 唯一約束
  4. 支付場景必須結合狀態机 + 對帳兜底
  5. 並發場景用 SETNX + 輪詢等待,不要用分散式锁

相關閱讀

權威參考

本站提供瀏覽器本地工具,免註冊即可試用 →

#API幂等性#分布式幂等#Idempotency Key#支付接口设计#面试高频#2026