Redis 秒殺|不能插隊的扣庫存與削峰
上一篇我們停在這張危險圖:兩個人都讀到「還剩 1」,各自減一,最後變成超賣。
問題不在「減一」這個動作本身,而在它被拆成兩步——看 與 改——中間還能被人插隊。
怎樣才算「不能插隊」?很簡單:櫃檯規定這兩步必須黏在一起。
從這裡開始,我們要的不是更快的「先讀再減」,而是:在 Redis 裡把查與扣黏成一次、別人插不進來的動作。
為什麼 Redis 適合當「不能插隊」的櫃檯
Redis 處理指令時,可以想成:櫃檯一次只完整服務一位客人。
這位客人的「查庫存 → 決定能不能賣 → 扣 1」若被綁成一套手續,下一位要等這套做完才輪到他——中間沒有「兩個人同時讀到 1」的空隙。
實務上,這套手續可以是 Redis 內建的原子指令,或一小段在伺服器端一次跑完的腳本——你只要記住目的:查與扣不要拆開給網路來來回回。
細節與程式碼請看專案:redis-seckill 的秒殺熱路徑;本系列主角仍是 Redis 的角色分工,不是腳本語法課。
熱路徑最小劇本
櫃檯那套手續,縮到最小大概長這樣:
成功分支只做兩件必要的事:改庫存,以及(常常)留下一張稍後處理的紙條。
紙條上可以寫:誰、買哪個、訂單編號、時間——夠 Worker 稍後把完整訂單與索引補齊就好。
熱路徑越短,轉圈圈越不容易回來。
為什麼不在熱路徑寫完整訂單?
若搶購成功的那一刻,還要順便做這些事:
- 把整張訂單存成一份完整紀錄
- 寫進「某用戶的歷史訂單」
- 更新熱銷排行榜
- 掛進後台分頁用的總索引
櫃檯手續會瞬間變長。人潮一來,你又把「單車道」從庫存格,搬到「寫一堆關聯資料」上——轉圈圈可能換個理由回來。
所以設計上常拆成兩種壓力:
- 熱點數字 — 必須立刻、正確地改完(上一節的原子手續)。
- 關聯寫入 — 重要,但不該全部堵在用戶還在轉圈圈的那幾百毫秒裡。
紙條,就是把第 2 種壓力「延後」的橋。
紙條落地:Redis Streams
在 Redis 裡,這張「稍後再記」的紙條,常見落點就是 Stream(串流/訊息流)。
心智模型:一條有編號的傳送帶。
- 熱路徑成功扣庫存後,把一張小卡片(誰、哪個商品、訂單 id、時間…)放上傳送帶,然後立刻回覆使用者。
- 旁邊的 Worker(工人) 依序取卡片,慢慢把完整訂單、索引、排行榜補齊。
這就是常說的削峰:瞬間湧進的寫入,變成傳送帶上的卡片,讓工人用自己的節奏處理,而不是全部堵在用戶還盯著轉圈圈的那一刻。
指令名稱(例如往 Stream 加一筆、工人用消費組讀取)記不記得沒關係;先記住 Redis 自己就能當這條傳送帶。實作見 redis-seckill 的 Worker。
整條路再看一次
把本篇拼起來,秒殺當下的資料流大概是這樣:
使用者只感受到 ① 的快慢;②③ 是系統在背後把尖峰寫入攤平、把資料補齊。
本篇小结
📝 TL;DR:用「黏成一次」的手續避免超賣;熱路徑只扣庫存並丟 Stream 紙條;Worker 稍後補寫訂單與索引,這叫削峰。
你帶走三件事:
- 原子性(直覺) — 查+扣不要拆開被人插隊。
- 熱路徑要短 — 完整訂單與一堆索引,別堵在轉圈圈那幾百毫秒。
- Stream + Worker — Redis 當傳送帶,把關聯寫入延後消化。
下一篇會講:Worker 補寫時,那些「索引」在 Redis 裡長什麼樣子——沒有 SQL 的 ORDER BY/JOIN,怎麼用 ZSet、List 做出後台分頁與排行榜。
專案:lucashsu95/redis-seckill。