🎯 什麼情境該想到我

當你「Redis 平常很快、但偶爾出現一波延遲尖峰」,或是正要下一個作用在不知道多大的 key 上的指令的時候。

⚙️ 怎麼用(步驟 / 公式)

前提:「單執行緒」是指命令執行

Redis 6.0 起網路 I/O 已支援多執行緒,但命令執行仍然是單執行緒。兩個後果:

  • ✅ 好處:單條命令天然原子,INCR 不需要鎖。
  • ❌ 代價:所有命令排一條隊。任何一條 O(N) 且 N 很大的命令,會讓後面所有請求一起等。

這不是「變慢」,是整台 Redis 在那段時間對所有 client 停止服務。監控上看到的是 p99 尖峰,不是平均值上升。

危險指令清單:複雜度裡有 N,而 N 你控制不了

指令複雜度替代做法
KEYS patternO(N),N = 整個 keyspaceSCAN cursor MATCH ... COUNT ...(游標分批)
HGETALL / SMEMBERSO(N),N = 集合大小HSCAN / SSCAN(自 2.8 起)
ZRANGE key 0 -1O(log N + M),M = 全部只取需要的區段;排行榜用 ZRANGE key 0 9 REV(ZREVRANGE 自 6.2 起已標為 deprecated,查證 2026-10-15)
LRANGE key 0 -1O(N)取固定區段,並用 LTRIM key 0 99 控制長度上限
DEL bigkeyO(N),要同步釋放所有記憶體UNLINK(4.0 起):O(1) 回收 key,記憶體釋放丟到背景執行緒
FLUSHALL / FLUSHDBO(N)加 ASYNC

查證 2026-09-25:UNLINK 自 4.0.0 起提供,官方複雜度說明是「O(1) for each key removed regardless of its size,之後在另一個執行緒做 O(N) 的記憶體回收」。
注意 lazyfree-lazy-user-del 預設是關閉的(0),所以直接下 DEL 依然是同步釋放——想讓 DEL 自動走 lazy free,要自己開這個設定。

★ 偷 Redis 的心法:把一次大操作拆成很多次小操作

Redis 自己就是這樣解決「擴容 Hash 表要搬幾百萬個桶」的問題——漸進式 rehash:

  1. 字典內部同時保留兩張雜湊表(舊表 + 新表)。
  2. 不一次搬完。之後每次對這個字典做增刪改查時,順手搬一部分桶。
  3. 舊表清空後,rehash 完成。

代價是 rehash 期間要同時維護兩張表(查找要查兩次、記憶體暫時變高),換到的是單次阻塞時間被攤平。

這個心法比 Redis 本身通用得多:任何單執行緒/單一事件迴圈的環境(Node.js 的 event loop、Go 的單一 goroutine worker、瀏覽器主執行緒),遇到「一次要處理很多」都該想想能不能攤成很多次小的。

實務檢查清單

  1. 上線前問自己:這個 key 最大會長到多大? 沒有答案就不要用 HGETALL 這類全取指令。
  2. 用 LTRIM / ZREMRANGEBYRANK 主動封頂,別讓集合無限成長(例如只留最新 100 筆動態)。
  3. 開慢查詢日誌:CONFIG SET slowlog-log-slower-than 10000(微秒),SLOWLOG GET 看誰在拖。
  4. 掃 keyspace 一律用 SCAN 家族,KEYS 只在本機 debug 用。
  5. 刪大 key 用 UNLINK,或事先確認 lazyfree-lazy-user-del 已開。

🧪 我實際套用的紀錄

  • 2026-09-25:(待填)

⚠️ 注意 / 什麼時候不適用

  • SCAN 是弱保證:保證掃描期間全程存在的元素至少被回傳一次,但可能重複回傳,期間新增/刪除的元素則不保證。消費端要能容忍重複。
  • UNLINK 不是萬靈丹。它把釋放丟到背景,但如果 key 是巨大的 listpack/embstr 這類單一大塊分配,背景執行緒也只是一次 free,省不了多少;真正有效的是成員數多的集合型 key。
  • 多執行緒 I/O 不會救你。6.0 的多執行緒只處理網路讀寫與協定解析,命令執行照樣排隊。
  • Lua 腳本與 MULTI/EXEC 期間是完全獨佔的,腳本裡寫迴圈掃大集合,等於自己造一個阻塞點。
  • 這張卡談的是單一實例的阻塞,不涵蓋叢集的 slot 遷移、主從複製斷線這類問題。

🔗 相關工具