Điều kiện tiên quyết: Đây là Phần 7 của Khóa Học System Design. Lội ngược dòng Phần 6: Khóa Phân Tán — ba cái trò chặn cửa tụi vãng lai đúp bóng (concurrent duplicate request blocking) múa may chung một mâm pháp bảo trói gô chèn ép (mutual exclusion primitives).

Answer-first: Đạo luật cản đúp API (API idempotency) vả vỡ mồm thề sống chết (ensures) phán rằng hễ lỡ tay chọt nút thử lại y boong mớ rác (retrying an identical request - móc chung thẻ Idempotency-Key) thì không bao giờ đẻ thêm trứng rơi vãi họa ngoài lề (additional side effects) lấn quá cái vạch chốt hạ của lần chọc đầu tiên (first execution). Miếng bùa này là thứ lõi móng cốt tử cho mấy cái cống rút tiền (payment APIs) nơi bão mạng đứt gãy bắt ép tụi khách chọc lút cán đập cửa (client retries), và nhỡ rách việc lọt lưới 1 cú nháy đúp (duplicate execution) thì đồng nghĩa với màn phang x2 cướp tiền thiên hạ (double charge).


Chìa Khóa Cản Đúp (Idempotency Key) Là Quái Gì?

Answer-first: Một cái Chìa Khóa Cản Đúp là một tấm thẻ bài độc đinh (unique token — thường chưng diện cúp UUID v4) — nặn ra từ tay khách sộp (client) và găm dính lên sọ bằng cái mũ Idempotency-Key HTTP header. Chủ quán xài thẻ bài này gác cổng chặn rác (detect duplicate requests): ngửi thấy thẻ này quen quen lướt ngang mặt (seen before), thì thảy thẳng cục rác tồn kho (cached response) từ cái bận đút lót lần đầu đập vô mặt (without re-executing) chớ rảnh đâu mà è cổ lôi máy ra xay lại (re-executing the handler).

Vì Sao Sống Chết chẳng Được Đàm Phán Cản Đúp (Non-Negotiable) Cho Lò Mổ Payment APIs

sequenceDiagram
    participant Client
    participant API as Cống Hút Tiền Payment API
    participant DB as Vựa Database

    Client->>API: Phóng nháy POST /payments {amount: $100} [Vác Thẻ: uuid-A]
    API->>DB: Nện cọc INSERT mớ payment record
    DB-->>API: Ngon Chim (SUCCESS)
    Note over API,Client: ⚠️ Sập tiệm mẻ mạng (Network timeout)! Khách móp mỏ đợi chẳng thấy ói ra biên lai.

    Client->>API: Ức chế đập cửa POST /payments {amount: $100} [Dùng Thẻ Cũ: uuid-A] (LÀM LẠI RETRY)

    alt ❌ Chơi ngu thả rong (Without idempotency)
        API->>DB: Đóng gạch táng INSERT payment record (Xong phim đẻ đúp!)
        Note over DB: Bóc lột túi tiền User vặt $200 thay vì hốt $100
    else ✅ Nẹp bùa khóa cản đúp (With idempotency key)
        API->>API: Lôi số gác cổng chốt: cái thẻ uuid-A này bào mòn rồi?
        API-->>Client: Trả lại rác cũ {status: success, tx_id: 123}
        Note over DB: Ngồi xơi nước chẳng bôi vẽ thêm DB — An Khang Thịnh Vượng!
    end

Nhìn Cảnh Phóng To (Scale context): Lễ hội lột xác Alipay Double 11 băm nát 583,000 nhát chém/giây lúc chóp đỉnh. Cơn bão đập cửa (Network retries) là thứ thảm họa bắt buộc rúc đầu (inevitable) ở cái tầm vũ trụ này — bùa cản đúp là cái khiên chặn họa chém hai dao (double charges). Băng đảng Stripe, Adyen, PayPal lăm le dí súng bắt trói Idempotency-Key ráo trọi cho toàn bộ ngóc ngách cống nạp cắn xé xào xáo (mutating endpoints).


Chiêu Trò Bày Binh Cản Đúp Kiểu Stripe (Stripe-Style)

Answer-first: Stripe nhồi giấu đống ngọc ngà châu báu dữ liệu chìa khóa (key metadata) vào mâm Redis (hot path, thẻ bài TTL 24 tiếng) đúc vào nguyên cục rác JSON chất đống (payload) ních no nê cái thông số mớ (status), mã vạch HTTP response code, vương miện headers, và cái xác không hồn body. Bóp nặn trét mớ bột dấu vân tay (payload hash) đặng soi ra mấy thằng lếu láo điềm nhiên vác chìa cũ mâm mới (key reuse) đánh tráo mớ xác thối rác rưởi (different request body). Lão già PostgreSQL bưng bít mớm vú cho cái hố sập lùi an toàn (durable fallback) rủi lỡ con Redis ngỏm củ tỏi (unavailable).

Cấu Trúc Khung Xương (Metadata Structure)

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "payload_hash": "băm_vụn_sha256_đầu_cổ_xác_request_body_bytes",
  "status": "completed",
  "response_code": 201,
  "response_headers": {
    "Content-Type": ["application/json"],
    "X-Transaction-Id": ["tx_987654"]
  },
  "response_body": "{\"transaction_id\":\"tx_987654\",\"status\":\"success\"}",
  "created_at": "2026-06-18T09:00:00Z",
  "expires_at": "2026-06-19T09:00:00Z"
}

Xưởng Đúc Đóng Bãi Đáp DB Fallback (Database Fallback Schema)

CREATE TABLE idempotency_keys (
    idemp_key        VARCHAR(255) NOT NULL,
    payload_hash     CHAR(64)     NOT NULL,         -- Đập móp băm nát SHA256 cái xác request body
    status           VARCHAR(50)  NOT NULL,          -- Rặn mãi: 'in-progress', 'completed', 'failed'
    response_code    INT,
    response_headers JSONB,
    response_body    TEXT,
    created_at       TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    expires_at       TIMESTAMPTZ  NOT NULL,

    PRIMARY KEY (idemp_key)
);

-- Soi mục lục hẹp (Partial index): chọc đúng ổ mấy cái khóa đang bốc cháy (active keys - tỉa gọt cắt xén cho mâm index thon gọn index size cho ba cái bảng thô kệch)
CREATE UNIQUE INDEX idx_idemp_active ON idempotency_keys (idemp_key)
    WHERE expires_at > NOW();

Phơi Ruột HTTP Middleware Kiếm Cơm Trọn Gói Trong Go

Answer-first: Đám lính lác chốt chặn cản đúp (idempotency middleware) nhảy xổ ra chẹn họng tất tần tật (intercepts all) mọi mớ thưa gửi vác vương miện Idempotency-Key header. Băng này múa võ Redis SetNX rạch mặt điểm tên đóng dấu khóa mõm chớp nhoáng (atomically claim) cái chìa khóa (triệt đường đám đúp bóng đánh lẻ concurrent duplicates), vác tải lấy cái thúng hứng rác (wraps the response writer) đặng đớp lượm trọn gói đống sản phẩm bôi ra (capture the output), sau chót quăng tuột cả mớ rác HTTP response sống nhăn vào chuồng Redis để chực chờ bãi nôn đợt sau (future replay).

package middleware

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/redis/go-redis/v9"
)

// IdempotencyRecord ngâm dấm nhồi lổm chổm JSON ở bãi Redis
type IdempotencyRecord struct {
    Status       string              `json:"status"`       // "in-progress" | "completed"
    ResponseCode int                 `json:"response_code"`
    Headers      map[string][]string `json:"headers"`
    Body         string              `json:"body"`
    PayloadHash  string              `json:"payload_hash"`
}

// responseRecorder bọc vỏ nhộng bao lấy http.ResponseWriter đặng túm mớ ọc rác (capture the response)
type responseRecorder struct {
    http.ResponseWriter
    code int
    body *bytes.Buffer
}

func newResponseRecorder(w http.ResponseWriter) *responseRecorder {
    return &responseRecorder{ResponseWriter: w, code: http.StatusOK, body: new(bytes.Buffer)}
}

func (r *responseRecorder) WriteHeader(statusCode int) {
    r.code = statusCode
    r.ResponseWriter.WriteHeader(statusCode)
}

func (r *responseRecorder) Write(b []byte) (int, error) {
    r.body.Write(b)
    return r.ResponseWriter.Write(b)
}

// IdempotencyMiddleware vác lề luật bóp cổ đám cản đúp giương ngạnh xài Redis SetNX
type IdempotencyMiddleware struct {
    rdb    *redis.Client
    keyTTL time.Duration
}

func NewIdempotencyMiddleware(rdb *redis.Client, keyTTL time.Duration) *IdempotencyMiddleware {
    return &IdempotencyMiddleware{rdb: rdb, keyTTL: keyTTL}
}

func (im *IdempotencyMiddleware) Handle(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        idempKey := r.Header.Get("Idempotency-Key")
        if idempKey == "" {
            next.ServeHTTP(w, r) // chẳng vác thẻ bài (No key) → mời quan anh lách (pass through)
            return
        }

        ctx := r.Context()
        redisKey := fmt.Sprintf("idemp:%s", idempKey)

        // Quay cối băm chả cục mìn (Compute payload hash) rình rập soi trò tráo trở xài lại thẻ vớ xác khác (key reuse with different body)
        body, _ := io.ReadAll(r.Body)
        r.Body = io.NopCloser(bytes.NewBuffer(body))
        hash := sha256.Sum256(body)
        payloadHash := hex.EncodeToString(hash[:])

        // Chặng 1 (Step 1): Dòm ngó thử thẻ này nát lỗ chưa (Check if key already exists)
        existingData, err := im.rdb.Get(ctx, redisKey).Result()
        if err == nil {
            var existing IdempotencyRecord
            if json.Unmarshal([]byte(existingData), &existing) == nil {
                // Tát sưng mỏ trò lén lút tuồn mìn mớ rác tráo xác (Reject key reuse with different payload)
                if existing.PayloadHash != "" && existing.PayloadHash != payloadHash {
                    http.Error(w,
                        `{"error":"idempotency_key_reuse","message":"Cầm chìa cũ ném rác mới (Key used with a different request body)"}`,
                        http.StatusUnprocessableEntity)
                    return
                }

                if existing.Status == "in-progress" {
                    // Cùng một lò goroutine/pod ất ơ nào đó đang è cổ nấu cơm với cái chìa khóa này
                    http.Error(w,
                        `{"error":"request_in_progress","message":"Trò chọc lố đang bị nhồi (Duplicate request is already being processed)"}`,
                        http.StatusConflict)
                    return
                }

                // Luộc lóng ngóng mớ phèo phổi (Already completed) — ọi trả cục rác (replay the cached response)
                for name, vals := range existing.Headers {
                    for _, val := range vals {
                        w.Header().Add(name, val)
                    }
                }
                w.Header().Set("X-Idempotent-Replayed", "true")
                w.WriteHeader(existing.ResponseCode)
                w.Write([]byte(existing.Body))
                return
            }
        }

        // Chặng 2 (Step 2): Nhắm mắt chém phát lụm luôn (Atomically claim) xài ngón đòn SetNX (SET if Not eXists)
        // DUY NHẤT MỘT THẰNG (ONE) lính goroutine/pod đớp được hũ vàng (succeed) — đám bậu xậu ra rìa húp cháo (false)
        inProgress := IdempotencyRecord{Status: "in-progress", PayloadHash: payloadHash}
        inProgressJSON, _ := json.Marshal(inProgress)

        set, setErr := im.rdb.SetNX(ctx, redisKey, inProgressJSON, im.keyTTL).Result()
        if setErr != nil || !set {
            http.Error(w,
                `{"error":"conflict","message":"Bị thằng ôn nhảy lầu chặn mẹ họng (Request already in progress)"}`,
                http.StatusConflict)
            return
        }

        // Chặng 3 (Step 3): Lăn xả nhào vô nấu rác (Execute the actual handler), hốt ổ trọn gói bãi mửa của nó
        recorder := newResponseRecorder(w)
        next.ServeHTTP(recorder, r)

        // Chặng 4 (Step 4): Nhồi rác vùi lấp (Save the completed response) chôn xuống Redis
        finalRecord := IdempotencyRecord{
            Status:       "completed",
            ResponseCode: recorder.code,
            Headers:      map[string][]string(w.Header()),
            Body:         recorder.body.String(),
            PayloadHash:  payloadHash,
        }
        finalJSON, _ := json.Marshal(finalRecord)
        im.rdb.Set(ctx, redisKey, finalJSON, im.keyTTL)
    })
}

Bùa Ngải Gì Giúp Trò SetNX Nắn Gân Kẹp Họng Chứng Giẫm Đạp Cấp Micro-giây (Microsecond Race Conditions)?

Answer-first: Đòn khóa hầu Redis SetNX chót lọt rớt trúng một cục gạch bóp chết nguyên khối (atomic operation) ở tầng command Redis — thậm chí lỡ 2 khứa goroutines mọc 2 sừng ở 2 lò pods khác biệt thi nhau réo ới vô đúng y chóc ở tích tắc cái bóng microsecond, thì con lừa Redis thong dong xếp xó lùa tất cả mớ hầm bà lằng (serializes all commands) bò vào hàng nối đuôi 1-ống-khói luồng xoáy cày mướn (single-threaded event loop). Duy nhất đét một gã (Exactly one) nhận phần thưởng nẫng true (set); đám trần ai lai khổ đứng khóc nấc cầm cùi bắp false (not set).

Màn Rải Bom Khảo Nghiệm Đạp Nhau (Concurrent Race Test)

package middleware

import (
    "net/http"
    "net/http/httptest"
    "sync"
    "sync/atomic"
    "testing"
    "time"
)

func TestIdempotencyMutualExclusion(t *testing.T) {
    var executionCount atomic.Int64

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        executionCount.Add(1)
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"transaction_id":"tx_001","status":"success"}`))
    })

    rdb := setupTestRedis() // cắn hàng rởm miniredis test ngáo 
    mw := NewIdempotencyMiddleware(rdb, 24*time.Hour)
    wrapped := mw.Handle(handler)

    const concurrency = 100
    var wg sync.WaitGroup
    wg.Add(concurrency)

    codes := make([]int, concurrency)
    for i := 0; i < concurrency; i++ {
        go func(index int) {
            defer wg.Done()
            req := httptest.NewRequest("POST", "/payments",
                strings.NewReader(`{"amount":100}`))
            req.Header.Set("Idempotency-Key", "same-uuid-for-all") // Cùng 1 mẹ đẻ ra! (Same key!)
            req.Header.Set("Content-Type", "application/json")

            rec := httptest.NewRecorder()
            wrapped.ServeHTTP(rec, req)
            codes[index] = rec.Code
        }(i)
    }
    wg.Wait()

    // Bọn thợ đụng cắn răng nhai phải chạy ĐÚNG DÀNH ĐÉT 1 PHÁT (exactly ONCE) mạc xác ngập mồm 100 cú chọt
    if count := executionCount.Load(); count != 1 {
        t.Errorf("Ngáo vãi đòi 1 cú, lòi mồm %d", count)
    }

    created, conflict := 0, 0
    for _, code := range codes {
        switch code {
        case http.StatusCreated:
            created++
        case http.StatusConflict:
            conflict++
        }
    }
    t.Logf("Lụm thóc: %d Trái (Created), %d Thọt Đè Nhau (Conflict) (đúc ở lò %d concurrent requests)",
        created, conflict, concurrency)
    // Sổ ghi: 1 con hốt bạc (Created), 99 đứa chui hầm mếu máo (Conflict)
}

Chiêu Ngáo Ộp Đập Cửa Rụt Dè Văng Trượt Cấp Số Nhân (Exponential Backoff) Và Rắc Trấu Ngẫu Nhiên (Jitter)

Answer-first: Đám bần nông khách điếm (Clients) BẮT BUỘC (must) nhồi phép rụt dè dãn gân văng lùi theo lốc lũy thừa (exponential backoff) chêm rắc tí hạt dẻ trấu ngẫu hứng (jitter) hòng gạt phăng mấy bãi rác bão đập cửa (retry storms) — cái trò nguyên một mớ bần cố nông xô đẩy vác gạch húc vào mâm ngay đúng chung một nén hương sau cú vỡ bờ mất mạng (outage). Vốc trấu (Jitter) rải lôm chôm lạng lách trộn nhiễu băm bổ cái mốc hãm trễ, tán mớ gạch rác rải đê tiện trườn theo thời gian (spreading traffic).

$$T_i = \min\left(T_{\text{max}},; T_{\text{base}} \times 2^{\text{mức_đập}} + \text{VãiĐạnNgẫuHứng Uniform}(0, J)\right)$$

package retry

import (
    "fmt"
    "math"
    "math/rand"
    "time"
)

type ExponentialBackoff struct {
    BaseDelay  time.Duration
    MaxDelay   time.Duration
    JitterCap  time.Duration
    MaxRetries int
}

func (b *ExponentialBackoff) NextDelay(attempt int) (time.Duration, bool) {
    if attempt >= b.MaxRetries {
        return 0, false
    }
    exp := math.Pow(2, float64(attempt))
    delay := time.Duration(float64(b.BaseDelay)*exp) +
        time.Duration(rand.Int63n(int64(b.JitterCap)))
    if delay > b.MaxDelay {
        delay = b.MaxDelay
    }
    return delay, true
}

// RetryWithIdempotency gồng mình đập cửa thử đâm lại (retries) CÙNG MỘT PHÔI THẺ cản đúp (SAME idempotency key) nhồi trên từng phát nã
func RetryWithIdempotency(
    key string,
    backoff ExponentialBackoff,
    fn func(idempKey string) (int, error),
) error {
    for attempt := 0; attempt < backoff.MaxRetries; attempt++ {
        statusCode, err := fn(key) // Chày cối 1 thẻ ở mỗi hiệp húc đụng (Same key on every retry — safe)

        if err == nil && statusCode < 500 {
            return nil // Êm chim (Success) quặc dính mìn client (4xx) — cuốn xéo về chẳng húc đập nữa (don't retry)
        }

        delay, more := backoff.NextDelay(attempt)
        if !more {
            return fmt.Errorf("cắn rứt nổ sọ hết trơn mẹ mìn đập cửa %d phát (exhausted retries)", attempt+1)
        }
        time.Sleep(delay)
    }
    return nil
}

Mổ Xẻ Sân Chơi (Case Study): Cái Áo Giáp Chống Đạn Đớp Tiền Alipay Double 11

🔥 [Miếng Nghề Bãi Xịn Production Pattern]: Đè bẹp mớ rác biz_no lủng cản đúp khét lẹt trần nhà Alipay Kích Cỡ Rác (Scale): Vã banh bướm 583,000 nhát xé họng/giây nổ cái đùng lóc chóc Double 11 2019 peak. Vũng Bệnh (Problem): Trò húc đập mạng lởm chởm ở cái vũng bùn này lôi ra đẻ sòn sòn hàng triệu vựa rác sinh đôi đúp rút tiền (duplicate payment attempts) nhồi sọ trong vòng 1 canh giờ. Kiến Trúc Bày Binh (Architecture): Lọt lòng mỗi phát nã đạn vác y chóc một cái bùa thẻ bài biz_no (mã danh rác làm ăn - business number) — đóng gạch phay ra hệt chóc Chìa Khóa Cản Đúp. Cái gầm mui Backend đè rác giấu nhẹm ném kho biz_no → result vô lõi lò OceanBase móc hàm thắt chóp chặn họng vây bằng vòng xích UNIQUE constraint. Lưới Kép Hai Cửa (Two-tier idempotency): (1) Ngõ lách Redis tạt nóng (hot path) — rọi mắt phanh phui < 1ms; (2) Lò vựa gạch đá OceanBase thủ dâm dự phòng hốt ổ (fallback) — ổ khóa đính chóp unique chặn ngửa cái ợ chua trượt tay (miss) Redis nào lòi rác cũng bị kẹp cổ trảm sống bởi cái màn hốt ứa lòi đúp ở bãi chăn ngựa DB-level. Hốt Ổ (Result): Trắng nõn sạch sẽ chẳng có cái tát văng mạng đòi đúp tiền (Zero duplicate charges) nào móc bóp sống nhăn ở khắp mấy cái bãi đáp Double 11. (Cào Lỗ Từ: Alibaba Cloud Architecture Blog)


Hỏi Nhanh Đáp Gọn (FAQ)

Rốt Cục Chìa Khóa Cản Đúp (Idempotency Key) Thuộc Cái Dạng Thá Gì?

Một miếng bài bùa (thường xài chung hệ UUID lỏ) giật được từ lò khách ất ơ phóng chui tọt thành cái đội mũ Idempotency-Key HTTP header. Chủ rạp lôi nó ra hốt ngửi mớ hàng dạt nhái đúp lại (duplicate requests). Hễ thẻ nào soi thấy mặt nát rồi: ói mửa ném trúng phóc cái hũ phản hồi ôi thiu lưu sẵn (cached response). Hễ hàng mới khui (new): bới nát, xay nhuyễn (process) và đóng thùng lưu kho (cache). Vòng đời sống ngắc ngoải (TTL) bết bát cũng được 24 tiếng.

Stripe Chơi Bùa Chú Độn Thổ Cản Đúp Xài Mánh Khóe Nào?

Stripe nhồi chèn bới lóc gầm giường thẻ khóa (mớ vương miện status, mã vạch response code, mũ áo headers, lọng body, và con ruồi nặn chữ payload hash) vứt phạch xuống đống Redis bồi 24-tiếng khóa mõm TTL. Ấn lướt status: in-progress bưng rào bít cửa đống giẫm đạp trèo cổng (concurrent duplicates). Chọt nặn payload_hash tát lật mặt đám xài thẻ cũ lùa ruột khác (key reuse with a different request body) (máng vào họng vả văng đạn HTTP 422). Cái vách sắt PostgreSQL giương ấn unique constraint làm bao cát bị bông bọc lót rủi ro nát mạng ngỏm cục Redis (unavailable).

Mày Tính Nuốt Sóng Bệnh Sida Đạp Nhau Cấp Micro-giây Bằng Kiểu Khỉ Mốc Nào?

Lưỡi gươm Redis SetNX rạch đét một nhát ập xuống (atomic) nhọn hoắt chóc ở lớp vỏ lệnh (command level) — Con lừa Redis trói gô rập khuôn (serializes) toàn bộ các chiêu đòn nhồi vào chung duy nhất 1 cái khe ống xoáy event loop đơn côi (single-threaded). Bét nhét chỉ chừa có duy nhất đét một gã (Only one) lính gọi mâm chen lọt giật cúp SetNX và đút túi bú được hàng true; 500 anh em tàn binh còn lại ăn cùi chỏ tọng false rồi mếu máo xách giỏ nhặt gạch HTTP 409 Conflict. Xong hỷ sự giật giải hốt ổ và cái bùa gõ status: completed được vùi vào, nguyên đám le ve đi sau đập cửa (future retries) lủi thủi nhai phải cục rác cũ ngâm dấm (cached response).


🔗 Bay Sang Bài Tới: Phần 8: Binh Pháp Lưới Saga & Giao Dịch Đứt Rải Tứ Tán Trong Go (Saga Pattern & Distributed Transactions in Go) — Dàn nhạc giao hưởng vung đũa chọc Temporal SDK (Temporal SDK orchestration), Hũ Trữ Tuồn Ngoài Kho Hàng Transactional Outbox, và Trò Đẩy Thuyền Đổi Đời Debezium CDC event routing.