Redis 秒殺|手動索引、分頁與排行榜
上一篇 Worker 負責「取紙條、把帳記齊」。
打開那隻工具箱,裡面通常至少有四樣東西要補:
四樣東西用途不同:
| 要回答的問題 | 補的是什麼 |
|---|---|
| 這張訂單內容是什麼? | 訂單本體 |
| 這個用戶買過哪些? | 用戶歷史 |
| 後台依時間翻頁怎麼翻? | 全域索引 |
| 哪個商品賣最好? | 排行榜 |
在 SQL 世界,很多關聯靠表格與索引自動維護。
在以 Redis 當主戰場的設計裡,這四樣常常要 Worker 寫入時自己維護——這就叫手動索引。下一拍起,我們逐個看它們長什麼樣子。
① 訂單本體:一張 key,一份內容
最直覺的一塊:訂單本體。
- Key 長得像門牌:
order:訂單編號 - Value 是這張訂單的完整內容(常見是一段 JSON 字串:誰買、買什麼、價格、時間…)
你有門牌,就能直接把整張訂單拿出來——這很快。
但新問題來了:後台要「依時間列出第 3 頁」時,你不可能靠猜門牌。
所以才需要旁邊那些索引(②③④)幫你回答「有哪些 id、怎麼排序」。
② 用戶歷史:一條屬於某人的隊伍
「這個用戶買過哪些?」不需要每次掃全部訂單。
常見做法:每位用戶一條 List(列表),裡面只放訂單 id。
- Key 像:
user:小明:orders - 新訂單來時,把 id 塞到隊伍最前面(最新的在前)
List 回答的是「有哪些門牌、大概的先後」。
真正的訂單內容,還是回到 ① 用門牌去取——索引薄、本體厚,各做各的事。
③ 全域索引:ZSet 當時間軸
後台要「依時間翻頁看全部訂單」時,需要一條全站共用的時間軸。
Redis 的 ZSet(有序集合) 很適合:每個成員是訂單 id,旁邊掛一個分數(score)——這裡用時間戳。
翻第 1 頁、第 3 頁,本質都是:在時間軸上切一段 id 出來。
不要對整庫做「把所有訂單門牌掃一遍」——那是另一種轉圈圈。
有了 id 列表之後,再一次把對應的訂單本體撈回來(下一節就講這兩步怎麼配)。
④ 排行榜:還是 ZSet,分數換成銷量
「哪個商品最好賣?」——又是排序問題。
於是排行榜也常用 ZSet:成員換成商品 id,分數換成銷量(或累計金額,看你怎麼定義「熱」)。
同一種籃子,兩種用法:
| 用途 | 成員是誰 | 分數代表什麼 |
|---|---|---|
| 全域訂單索引 | 訂單 id | 時間 |
| 熱銷排行榜 | 商品 id | 銷量/熱度 |
不需要每次用 SQL 掃表再 ORDER BY——分數更新時,排序關係已經在 ZSet 裡。
兩步讀法:先門牌,再一次拿內容
無論是用戶歷史(List)還是後台時間軸(ZSet),讀法都差不多:
- 先從索引拿到一串訂單 id
- 再依這串 id,一次把多份訂單本體撈回來
為什麼不把整包 JSON 塞進索引?
因為同一份訂單會出現在「用戶歷史」和「全域時間軸」——本體存一份就好,索引只負責指路。批次讀(常對應一次拿多個 key)就是把路牌變成內容的那一步。
刪除時要掃乾淨
手動索引的代價是:你寫了幾份,刪的時候也要記得幾份。
只刪 order:o101 本體,卻忘掉:
- 全域時間軸上的
o101 - 某用戶 List 裡的
o101
後台翻頁就會點到幽靈門牌——有索引、沒內容。
實務上會把這些刪除綁成一次事務/一次腳本,避免清到一半失敗。細節見專案後台刪除流程:redis-seckill。
本篇小结(與系列收束)
📝 TL;DR:Worker 用手動索引維護本體、用戶歷史、時間軸、排行榜;讀資料是「先 id 再批次拿本體」;刪除要連索引一起清。
你帶走四件事:
- 本體一份、索引多份 — String 存內容;List/ZSet 負責指路與排序。
- ZSet 兩用 — 分數=時間做分頁;分數=銷量做排行榜。
- 兩步讀 — 索引給 id,再一次取多份本體。
- 刪要掃乾淨 — 否則幽靈門牌。
三篇地圖
| 篇 | 檔案 | 記住什麼 |
|---|---|---|
| 1 | redis-seckill-intro | 轉圈圈、Redis 第一線、又快又不能算錯 |
| 2 | redis-seckill-streams | 原子扣減、Stream 削峰、Worker |
| 3 | 本篇 | 手動索引、分頁、排行榜、刪除一致性 |
尚可繼續探索(專案 README 也有提):Worker 失敗時的重試、死信、庫存回補——那是「紙條丟了怎麼辦」的下一層。
完整實作:lucashsu95/redis-seckill。