Intro
系統設計
-
必要路徑:解決「快」與「不超賣」
User -> Redis (DECR/Lua) -> MQ -> DB worker -> DB -
完整路徑:解決「安全」與「穩定」
User -> CDN/WAF -> Gateway -> Redis (DECR/Lua) -> MQ -> DB worker -> DB
| 層級 | 核心技術 | 目的 |
|---|---|---|
| 入口層 (Entry Layer) | CDN + WAF + Queue-it | 阻擋惡意流量、過濾靜態資源、排隊放行 (Virtual Waiting Room) |
| 應用層 (Application Layer) | Microservices + API Gateway | 業務邏輯解耦、實施熔斷與限流策略 |
| 緩存層 (Caching Layer) | Redis (Lua Scripts) | 實現原子性預扣庫存、支撐高併發讀取請求 |
| 訊息層 (Messaging Layer) | Kafka / RocketMQ | 非同步寫入資料庫、削峰填谷、緩解 DB 瞬時壓力 |
| 數據層 (Data Layer) | MySQL (Shard/Split) | 訂單記錄持久化、確保最終一致性 |
設計要點
User 限制
實作基於 Redis 的分佈式冪等 (Idempotency) 鎖機制:搶票請求可以用 user_id 與 event_id 作為複合 Key,利用 SETNX (或 SET ... NX EX) 發起 Atomic 鎖定請求,時效 (TTL) 需大於業務處理的最長期望時間)。若回傳失敗,應立即觸發『快速失敗 (Fail-fast)』機制,回傳請求處理中或重複提交。鎖定成功後才可進入庫存預扣流程。
鎖的釋放建議配合業務條件設計是在 DB 處理完之後。
Redis 庫存預扣機制 (Inventory Pre-decrement)
為避免無效請求堆積於 MQ 並減輕資料庫壓力,系統建議採用「緩存優先」的過濾策略:
-
資料預熱 (Data Warm-up):
- 活動開賣前,將資料庫中的「可用庫存數」與「活動狀態」同步至 Redis 叢集。
- 若 Redis 中無此活動 Key,系統應直接攔截請求,禁止穿透至資料庫。
-
原子扣減 (Atomic Operations):
- 採用 Lua 腳本 封裝庫存檢查與扣減邏輯 (
if stock > 0 then redis.call('DECR', key) ...)。 - 僅在 Redis 預扣成功後,系統方可產生「預選位」訊息並發送至 MQ。
- 採用 Lua 腳本 封裝庫存檢查與扣減邏輯 (
-
削峰填谷 (Traffic Shaping):
- 透過 Redis 過濾掉 90% 以上的「未搶到」請求,確保進入 MQ 的每一則訊息均為「潛在有效訂單」,大幅提升後端 DB Worker 的處理效率。
-
異常回補 (Inventory Compensation):
- 建立反向回補機制:若後端訂單因支付超時、用戶取消或系統異常而失敗,必須主動執行 Redis
INCR操作,將庫存釋放回緩存池供後續用戶搶購。
- 建立反向回補機制:若後端訂單因支付超時、用戶取消或系統異常而失敗,必須主動執行 Redis
配合 User 限制,Lua 腳本範例可以如下:
-- KEYS[1]: stock_key (庫存的 Key)
-- KEYS[2]: user_set_key (記錄已中籤用戶的 Set Key)
-- ARGV[1]: user_id (當前請求的用戶 ID)
local stock_key = KEYS[1]
local user_set_key = KEYS[2]
local user_id = ARGV[1]
-- 1. 檢查用戶是否重複搶購
-- 使用 SISMEMBER (Set Is Member):
-- 作用:在 $O(1)$ 時間內檢查指定元素 (user_id) 是否存在於集合 (Set) 中。
-- 目的:確保業務冪等性,防止同一用戶重複扣減庫存。
if redis.call('SISMEMBER', user_set_key, user_id) == 1 then
return -1 -- 代表該用戶已存在於中籤名單中
end
-- 2. 檢查庫存量
-- 使用 GET (Get):
-- 作用:獲取指定 Key 的字串值。
-- 目的:讀取當前剩餘的可用票數。
local stock = tonumber(redis.call('GET', stock_key) or "0")
if stock > 0 then
-- 3. 執行扣減庫存
-- 使用 DECR (Decrement):
-- 作用:將 Key 中儲存的數字值減一。此操作具備原子性。
-- 目的:正式在緩存層佔用一個名額。
redis.call('DECR', stock_key)
-- 4. 記錄中籤用戶
-- 使用 SADD (Set Add):
-- 作用:將一個元素加入集合中。如果元素已存在,則忽略。
-- 目的:將成功扣減庫存的用戶 ID 永久(或直到活動結束)記錄下來。
redis.call('SADD', user_set_key, user_id)
return 1 -- 成功預扣庫存
else
-- 庫存不足
return 0 -- 代表已售罄
end
Redis 使用單執行緒模型執行腳本。當一個腳本在執行時,Redis 會阻塞所有其他客戶端的指令,直到該腳本執行完畢。