01

總覽與定位

Redis 是單執行緒的記憶體內資料結構伺服器。本篇講它自己怎麼跑、怎麼活過重啟、怎麼從一台長成一群——不重講應用層怎麼 Cache-Aside。

把 Redis 當「比較快的 Memcached」會漏掉它真正的契約:命令在單一事件迴圈裡依序執行,資料以型別與編碼住在行程記憶體裡,持久化、複寫、叢集都是這份記憶體的外掛,不是另一套資料庫。應用層讀寫模式(Cache-Aside、Write-Through)已經在 快取策略 講完;穿透、擊穿、雪崩的防護在 快取失效模式與雪崩防護。本篇站在 Redis 行程內部:執行模型、編碼、落盤、主從、slot、maxmemory。

1 · 怎麼執行
Event loop
一次一條命令。複雜度是延遲帳單;I/O thread 不解這個單執行緒契約。
2 · 怎麼存
SDS / Hash / ZSet / Set
同樣 GET,底層編碼不同,記憶體差一個數量級。選型先選編碼,再選 API。
3 · 怎麼活過重啟
RDB · AOF · Replica
快照、指令日誌、複本是三條不同的復原語義。Sentinel 管的是誰接寫。
4 · 怎麼水平切
16384 slots
固定 slot 表,不是 hash ring。遷移靠 MOVED/ASK,客戶端必須會重定向。
flowchart LR
    C["Client"] --> E["單執行緒事件迴圈"]
    E --> M["記憶體資料結構"]
    M -. "fork 後背景寫出" .-> P["RDB / AOF"]
    M -. "命令流 + backlog" .-> R["Replica / Cluster"]
  

所有命令都穿過同一條單執行緒事件迴圈;持久化與複寫是掛在記憶體之後的兩條旁路(虛線),不在命令的關鍵路徑上——這也是為什麼一個 O(N) 命令就能卡住全部人,而 AOF rewrite 卻只在 fork 那一瞬間才痛。

座標本篇不重複的四件事

Cache-Aside、Read-Through、TTL vs LRU 的應用層語意交給 快取策略。
Bloom、負快取、單 key 互斥重建、TTL jitter 交給 快取失效模式與雪崩防護。
Hash ring、虛擬節點、節點增減時 key 遷移量,交給 一致性雜湊——Cluster 只用固定 16384 slot 與 MOVED/ASK。
同步/異步複寫、R+W>N、版本向量交給 資料複寫基礎 與 進階複寫一致性;這裡只談 Redis 自己的 backlog、offset 與 replica lag。

一句話答法Redis 是單執行緒記憶體資料結構伺服器。快取模式決定你怎麼用它;本篇決定它當機、切主、切槽、撞 maxmemory 時實際發生什麼。
02

單執行緒事件迴圈與命令複雜度

所有命令走同一條迴圈。延遲不是「機器慢」,是上一條命令還沒做完。

Redis 主執行緒用 I/O 多工(epoll/kqueue)收 socket,從輸入緩衝區解析出一條 RESP 命令,執行,再把回覆寫回。執行期間不切換到下一條命令。這讓每個 key 的操作天然原子——INCR 不需要應用層鎖——代價是:一條 O(N) 命令會讓後面所有客戶端一起排隊。

p99 latency ≈ 排隊中所有命令的 CPU 時間之和(同一條迴圈)

1 · 哪些命令是延遲炸彈

命令 複雜度 何時會卡住迴圈
GET / SET / INCR / HGET O(1) 正常路徑。這是 Redis 低延遲的來源
KEYS * O(N) 全庫 生產環境等同停機。用 SCAN 分批
HGETALL / SMEMBERS / LRANGE 0 -1 O(N) 該 key 百萬 field 的 Hash 一次拉完,迴圈被這一個 key 綁住
DEL 大 key O(N) 釋放 同步釋放物件圖。改 UNLINK(lazy free)把釋放丟給背景執行緒
FLUSHALL / 大量 EXPIRE 視實作 過期是惰性刪除 + 定時抽樣,不是「時間到自動免費」

O(N) 的 N 是「這個結構有多少元素」,不是「你有多少台機器」。集群切再細,單一 Hash 百萬 field 仍會卡住那一台的迴圈。

2 · Redis 6 的 I/O threads 不解這個契約

I/O threads 只分擔 socket 讀寫:把位元組從網卡搬進緩衝、把回覆寫出去。命令執行、物件編碼、過期抽樣、eviction 候選,仍在主執行緒。io-threads-do-reads 打開之後,benchmark 的吞吐量會升,但 KEYS 照樣阻塞所有人。

陷阱「單執行緒所以一定慢」與「開了 I/O thread 所以是平行資料庫」都錯

單執行緒在記憶體操作上極快:沒有鎖、快取親和、原子語意免費。慢的是你丟進去的命令複雜度與大 key。面試被問「Redis 為什麼單執行緒還那麼快」,答:執行模型適合記憶體 O(1) 操作;被問「如何水平擴充讀」,答 replica 與 Cluster,不是把主執行緒改成多執行緒跑命令。

一句話答法Redis 主迴圈一次跑一條命令,原子性與低延遲同源。生產禁用 KEYS,大集合用 HSCAN/UNLINK;I/O threads 只搬位元組,不平行執行命令。
03

資料結構選型

對外四種常用型別,對內各有兩套編碼。記憶體帳單在編碼,不在型別名。

同一個「使用者資料」,SET user:1 "{json}"、HSET user:1 name …、ZADD rank score member 佔的記憶體與能做的查詢完全不同。選型規則是:先列出要做的操作(單點讀、欄位更新、範圍、成員測試),再選型別;最後確認編碼會不會在資料長大時從緊湊結構升級成散列表/跳表。

型別 緊湊編碼 長大後 適合 不適合
String embstr / raw SDS 單一緩衝 快取 blob、計數器、INCR、短 TTL session 經常改 JSON 裡一個欄位——每次都重寫整串
Hash listpack(舊稱 ziplist) hashtable 物件欄位局部更新、HINCRBY、field 級 TTL(依版本) 把整張表塞進一個 Hash 再 HGETALL
ZSet listpack skiplist + hash 排行榜、延遲佇列(score = 時間)、範圍查詢 只測「在不在」——那是 Set 的工作
Set intset hashtable 去重、標籤、交集/差集、隨機抽樣 需要順序或分數;整數集合一旦混入非整數就升級

listpack/intset 是為了 CPU cache 與指標開銷:小集合連續存放。一旦超過 hash-max-listpack-entries 等閾值,就變成真正的 hashtable/skiplist,記憶體會跳一截——這是「昨天還好好的,今天 RSS 爆了」的常見原因。

選型口訣
操作 → 型別 → 編碼閾值
  • 整份文件、很少改欄位:String + 應用層序列化
  • 常改單一屬性:Hash,避免每次序列化整包 JSON
  • 要 top-N 或時間排序:ZSet,score 就是排序鍵
  • 只要唯一集合:Set;純整數 ID 可吃到 intset 的便宜
記憶體陷阱
指標與過期字典比 payload 貴
  • 百萬把 16-byte 的短 key,過期字典與 SDS header 可能比 value 還大
  • Hash 的每個 field 都有獨立開銷;欄位極多時,拆成多把 Hash 或改文件型儲存
  • ZSet 同時維持 skiplist 與 hash,同一 member 存兩份索引

左欄是選型時該按的順序(先問操作、再選型別、最後才看編碼閾值),右欄是同一個決定在記憶體上的代價。對照著看會發現:真正讓 RSS 跳一截的不是 payload,而是超過閾值後從 listpack/intset 換成 hashtable/skiplist 所帶來的指標開銷。

關鍵Big key 往往是選型錯誤,不是流量錯誤

一個 8 MB 的 String 當使用者資料、一個 50 萬 member 的 ZSet 當「全站排行」,讀寫都會卡主迴圈,複寫與 RDB 也會被這一個 key 拖住。正確做法是把大結構按查詢邊界切開(按時間分片的 ZSet、按 field 前綴切開的 Hash),而不是先加 replica 指望讀擴散——replica 仍要完整複製那把大 key。

一句話答法String 放 blob、Hash 放會局部更新的物件、ZSet 放有序、Set 放去重。先看操作再看編碼升級閾值;大 key 是選型問題,加機器解不掉。
04

持久化:RDB 與 AOF

記憶體是真相來源;RDB 與 AOF 是兩種不同的「死了能救回多少」契約。

不開持久化的 Redis 重啟後是空的——這對純快取可以接受(回填交給應用,見 快取策略)。一旦 Redis 被當成 session、佇列、排行榜的權威,你必須回答兩個問題:崩潰時最多丟多久的資料,以及重啟要多久才能再接寫。

RDB
時間點快照
  • 子行程 fork,利用 OS copy-on-write 把當下的 keyspace 寫成壓縮的 .rdb
  • 復原快、檔案小,適合備份與跨機房種子
  • 上次快照之後的寫入,崩潰就沒了。間隔是你設的 save 規則
  • fork 當下的 RSS 會跳:寫入密集時 COW 讓父子各留一份髒頁
AOF
命令日誌
  • 每條寫命令追加到日誌。重啟時重放,狀態回到「日誌裡最後一筆 fsync 成功的寫入」
  • appendfsync always:每命令 fsync,幾乎不丟,延遲最高
  • everysec(預設):最多丟約一秒。這是大多數生產的折衷
  • no:交給 OS。崩潰窗口不確定
  • 日誌會膨脹,靠背景 rewrite 壓回;Redis 4+ 可用 RDB preamble 加速 rewrite

兩欄的分野是「崩潰之後還剩什麼」:RDB 給的是一個完整但過去的時間點,AOF 給的是一條可重放的命令流。看 AOF 那欄的 appendfsync 三行——資料安全的刻度就在那裡,其餘都是還原速度與檔案大小的取捨。

設定 崩潰最多丟 寫入延遲 重啟速度
無持久化 全部 最低 瞬間(空的)
僅 RDB,例如 60 秒 最多一個快照間隔 低(背景 fork) 快(載入快照)
AOF everysec 約 1 秒 中 中(重放;有 RDB preamble 則快很多)
AOF always 約 0 高 同 AOF

兩種都開時,重啟預設以 AOF 為準(它更新)。面試答「開了 RDB 就不會丟資料」是錯的:RDB 保證的是「有一份過去的完整點」,不是「最後一筆寫入還在」。

陷阱fork 與磁碟是隱藏的延遲來源

寫入很忙時做 RDB/AOF rewrite,子行程 COW 會讓實際記憶體接近 2×,Linux 可能在 overcommit 不允許時直接拒絕 fork——於是備份失敗,而主迴圈還在。磁碟慢則 AOF fsync 回壓主執行緒(everysec 也會在上一秒還沒 sync 完時記錄延遲)。觀測要用 latest_fork_usec、aof_delayed_fsync,不要只看 QPS。

一句話答法RDB 是可攜的時間點備份,崩潰丟「上次快照之後」;AOF everysec 是預設的近即時復原,最多丟約一秒。兩者一起開時,復原聽 AOF。
05

主從複寫與 Sentinel

Replica 複製的是 Redis 自己的命令流與 backlog,不是通用的 quorum 協議。Sentinel 只決定誰變成 master。

一主多從是 Redis 最常見的高可用形狀:寫進 master,replica 用非同步命令流追上。通用的同步/異步取捨與「讀到什麼」的一致性光譜,資料複寫基礎 已經講過。這裡只留下 Redis 專屬的三個旋鈕:PSYNC 能否部分重同步、replica lag 怎麼觀測、Sentinel 如何選主而避免腦裂。

1 · 全量 SYNC 與部分 PSYNC

flowchart TD
    A["Replica 重新連上"] --> B["送 PSYNC + offset"]
    B --> D{"offset 還在 backlog?"}
    D -- "是" --> E["部分重同步"]
    D -- "否" --> F["master fork RDB"]
    F --> G["期間寫入進 backlog"]
    G --> E
  

分岔點只有一個:replica 的 replication offset 是否還落在 master 的 repl_backlog 窗口內。掉出窗口(長斷線、backlog 太小、master 重啟換了 runid)的帳單是 master 再 fork 一份 RDB——這才是「replica 閃斷會拖垮 master」的來源。

Master 維持一份固定大小的 repl_backlog。Replica 短暫斷線後,若它的 replication offset 仍在 backlog 窗口內,就只補那一段(PSYNC)。窗口外——長斷線、backlog 太小、master 重啟換了 runid——就走全量:master 再 fork 一份 RDB 傳過去,期間寫入仍進 backlog,傳完再補增量。全量期間 master 的 CPU、fork、網路都是帳單;這是「為什麼 replica 閃斷會拖垮 master」的原因。

2 · 延遲觀測與寫入保護

  • INFO replication 的 master_repl_offset 與 replica 的 slave_repl_offset 相減,就是以位元組計的落後。
  • master_last_io_seconds_ago 看連線是否活著;offset 接近但秒數很大,常常是沒寫入,不是健康。
  • min-replicas-to-write + min-replicas-max-lag:落後超過閾值的 replica 不計入,寫入會被拒絕。這是 Redis 版的「不要在沒有足夠新複本時承認寫入」——不是 quorum 協議,是 master 自己數人頭。
  • 讀 replica 預設可能讀到舊值。需要讀已確認寫入,讀 master,或在應用層等 offset。

3 · Sentinel 故障轉移

Sentinel 是一組獨立行程,對同一份 Redis 拓撲做監控。主觀下線(SDOWN)只是「我連不上」;客觀下線(ODOWN)要達到設定的 quorum 個 Sentinel 同意。然後 Sentinel 之間再選一個 leader 來執行 failover:挑 replica(看優先級、offset、runid)、SLAVEOF NO ONE、通知其他 replica 改追新主、通知客戶端。

要設對的
讓多數派存在且唯一
  • Sentinel 至少 3 個,quorum 為多數。兩個機房對半切時,少數側不該能單獨 failover
  • replica-priority 0:這台永不被提升(通常是跨區唯讀、或資料明顯落後的分析 replica)
  • 客戶端必須訂閱 Sentinel 的切主事件,或走會更新拓撲的代理;寫死 master IP 等於沒做 HA
腦裂長什麼樣
舊主在分區另一側仍接寫
  • 網路切成兩半,少數 Sentinel 與舊主在一起:若 quorum 不夠大,可能出現兩邊都以為自己是主
  • 即使 Sentinel 做對,舊主在被降級前仍可能接受寫入。那些寫入在重新成為 replica 時會被丟掉
  • 這不是 Raft:沒有 fencing token 自動作廢舊主的寫入。重要寫入要能冪等,或接受「failover 窗口會丟」

左欄是讓「多數派存在且唯一」的設定,右欄是這組設定沒做對時的長相。重點在右欄第一行:腦裂的成因不是 Sentinel 數量不夠,而是舊主在分區另一側仍然接受寫入——所以要靠 min-replicas-to-write 這類寫入保護把它自己關掉。

關鍵Sentinel 管角色,不管 slot

Sentinel 適用「一台邏輯 master + 若干 replica」。資料仍在同一份 keyspace。要水平切資料,用 Cluster(第 6 節),不要把 Sentinel 與 Cluster 疊成同一套拓撲——兩者的客戶端協定、故障轉移與組態來源都不同。

一句話答法Replica 靠 backlog 做 PSYNC,窗口外就全量 RDB。Sentinel 用 quorum 宣告 ODOWN 再選主;舊主在分區窗口寫入的資料,failover 後會丟,這不是 Raft。
06

Cluster:16384 slot 與線上 resharding

Cluster 把 keyspace 切成固定 16384 個 slot。遷移的單位是 slot,客戶端靠 MOVED 與 ASK 找到新家。

一致性雜湊用 ring 與虛擬節點,讓節點增減時只搬鄰近的 key——那是 一致性雜湊 的題。Redis Cluster 不走那條路:它預先切好 16384 個槽,每個槽指到一台主節點。新增節點 = 從既有節點搬走一些槽;刪節點 = 把它的槽交給別人。Hash 函式只決定 key 進哪一槽,不決定槽在哪一台機器。

slot(key) = CRC16(hash_tag_or_key) mod 16384

花括號 hash tag 讓相關 key 進同一槽:{user:100}:profile 與 {user:100}:cart 都只雜湊 user:100,於是 MULTI/WATCH 這類單槽事務才可能成立。沒有 tag 的跨 key 事務,Cluster 會直接拒。

1 · 客戶端必須是 cluster-aware

客戶端本地快取一份 slot → node 對照表。第一次可以對任意節點下命令;之後靠兩種重定向自我修正:

回應 何時出現 客戶端該做什麼
MOVED slot host:port 槽的歸屬已永久改到該節點 更新本地對照表,把命令重打到新節點。之後同一槽走新家
ASK slot host:port 槽正在遷移:本節點是來源且這把 key 已經被搬走 這一次帶 ASKING 去目標節點。不要改對照表——槽還沒搬完,下一把 key 可能還在來源
TRYAGAIN 叢集忙碌或遷移中無法服務 退避重試,不要當成應用錯誤

L4 負載平衡把連線打到隨機節點,卻期望「永遠打對槽」,會讓每一條命令都先被 MOVED 一次。正確做法是 client-side 對照表,或懂 cluster 協定的 L7 proxy。

2 · 線上遷移:MIGRATING / IMPORTING

管理員把 slot 7 從節點 A 遷到 C 時,A 標成 MIGRATING,C 標成 IMPORTING,然後一把一把 MIGRATE(實作上接近 DUMP → RESTORE → DEL)。期間:

  • key 還在 A:A 直接服務。客戶端對照表仍指向 A,這是 HIT。
  • key 已到 C:A 回 ASK C。客戶端這一次去 C;沒更新對照表。
  • 整槽搬完、SETSLOT 7 NODE C:歸屬切到 C。之後任何人問 A,得到 MOVED C,對照表才改。
sequenceDiagram
    participant CL as Client
    participant A as 節點 A
    participant C as 節點 C
    Note over CL,C: 遷移中:A MIGRATING、C IMPORTING
    CL->>A: GET cart:42,對照表指向 A
    A-->>CL: ASK C
    CL->>C: ASKING + GET
    C-->>CL: 回值
    Note over CL: 對照表不變,下一次仍先問 A
    Note over CL,C: SETSLOT 7 NODE C 之後
    CL->>A: GET cart:42,對照表仍指向 A
    A-->>CL: MOVED C
    CL->>C: GET cart:42
    C-->>CL: 回值
    Note over CL: 對照表改寫,之後直打 C
  

兩趟都先打到 A,差別在回程之後:ASK 不改對照表,下一次仍先問 A;MOVED 才讓客戶端把 slot 7 記到 C。所以 ASK 是「這一次借道」,MOVED 是「歸屬換人」。

3 · 互動:在 16384 格上看歸屬、傾斜與重定向

互動 · 16384 slot 空間 調節點數與遷移,輸入 key 看它落在哪一格

下面這張格帶就是完整的 keyspace:128 欄 × 128 列 = 16384 格,一格一個 slot(左上是 slot 0,右下是 slot 16383),顏色是歸屬節點。key 的 slot 由 CRC16 當場算出,不是查表。

slot 0每列 128 格slot 16383
N1 N2 N3 N4+ 排隊待遷(仍屬來源) 正在搬(MIGRATING/IMPORTING)

在格帶上點一下,可查該格的 slot 號與歸屬。

3 台
MIGRATING
IMPORTING
512 格
40%

「填一把落在遷移段的 key」是真的從 job:1 開始逐一算 CRC16,優先找落在正在搬的那一格、而且會拿到 ASK 的那一把(搜尋上限 20 萬把,按下去大約要等 0.3 秒)。

三個可驗的點:(1) 加一台的搬遷量正好是 16384 ÷ 新節點數——Redis 只從既有各節點的尾端切走要給新節點的那一份,其餘 slot 不動,所以新節點在格帶上是好幾段同色而不是一整條;這與一致性雜湊的鄰近重映射殊途同歸,但靠的是固定對照表。(2) 同一把 key 在遷移進度 0%/中途/100% 會依序得到 HIT、ASK、MOVED,三者由「它的 slot 排在遷移清單的第幾格」算出。(3) 「正在搬的那一格裡,這把 key 到底搬走了沒」現實中取決於 MIGRATE 跑到哪;本 demo 為了可重現,改用該 key 的 CRC16 落在 slot 索引之外的高位當代理規則(低 14 位就是 slot 本身,對同一格的所有 key 都相同,拿來當代理會讓整格答案一致)——所以同一格裡不同的 key 會拿到不同答案,這正是遷移窗口裡 MGET 會一半 ASK、一半本地的原因。

陷阱遷移窗口的多 key 操作與「以為 hash ring 會自動平衡」

MIGRATING 時,同一槽裡有的 key 在 A、有的在 C。客戶端若對兩把 key 做一次 MGET,可能一半 ASK、一半本地——這就是為什麼遷移期要避免跨 key 批次,或接受重試。另外:新增節點不會自動把熱 slot 拆開。熱 key 若都落在同一槽(沒設計 hash tag),那一槽再怎麼遷,流量仍綁在一台。熱槽要靠拆 key,不是靠多加節點。

一句話答法Cluster 是 16384 格固定表,不是 hash ring。MOVED 更新對照表;ASK 只重試這一次。線上 resharding 一次搬一個 slot,遷移期來源與目標同時活著。
07

maxmemory、淘汰策略與大/熱 key

記憶體上限是硬牆。牆撞上時,Redis 不是變慢,是開始丟 key 或拒絕寫入。

maxmemory 把 RSS 目標設死。超過之後,下一個會增加記憶體的命令先走 eviction:依政策挑犧牲者,直到騰出空間,或發現政策不允許而回 OOM。這與 快取策略 裡「快取滿了用 LRU 踢」是同一類問題,但政策名稱與「哪些 key 有資格被踢」是 Redis 產品細節。

政策 候選集合 挑誰 寫入撞牆時
noeviction — — 回 OOM,讀仍可
allkeys-lru / allkeys-lfu 全部 key 最久未碰 / 最低頻 踢掉後寫入成功
volatile-lru / volatile-lfu 僅設了 expire 的 key 同上,但只在有 TTL 的子集 沒有可踢的就 OOM
volatile-ttl 僅設了 expire 的 key TTL 最短的先走 持久 key 永不被這條政策踢

「session 設 TTL、排行榜不設」時用 volatile-*,撞牆只犧牲 session。兩邊都不設 TTL 卻開 volatile-lru,等價於 noeviction——這是上線後寫入突然 OOM 的經典誤配。

大 key 與熱 key

大 key
單鍵太大,卡住迴圈與複寫
  • 偵測:MEMORY USAGE、redis-cli --bigkeys、慢日誌裡的 HGETALL/LRANGE
  • 處置:按查詢邊界拆鍵(Hash 按 field 前綴、ZSet 按時間桶),刪除用 UNLINK
  • 複寫與 RDB 都會完整搬這把 key——replica 救不了大 key
熱 key
單鍵 QPS 打滿一台的迴圈
  • 偵測:redis-cli --hotkeys(需 LFU 政策或等效計數)、代理層 per-key QPS
  • 處置:本地進程快取那一把、讀打 replica、把 value 複製成 key#1 … key#N 打散讀
  • Cluster 幫不上忙:同一 key 永遠同一槽、同一主節點

左右兩欄是兩種不同的病:左邊是單鍵太大(一次命令就卡住整條迴圈,複寫與 RDB 也被它拖住),右邊是單鍵太熱(key 不大,但流量全壓在同一個 slot 的同一台主節點)。看處置那一列就知道差別:大 key 要拆結構,熱 key 要複製或打散,兩者都不是加節點能解的。

關鍵熱 key 的應用層互斥重建不是本篇的題

快取 miss 瞬間打穿 DB、用 singleflight/mutex 合併回源,是 快取失效模式與雪崩防護 的範圍。這裡的熱 key 是「Redis 自己的這把 key 被讀寫到主迴圈飽和」——即使 DB 完全沒被打到,這台 Redis 也已經是瓶頸。兩個層次都要看,但解法不同。

一句話答法allkeys-lru 什麼都可踢;volatile-ttl 只踢有過期的,持久資料撞牆就 OOM。大 key 要拆,熱 key 要打散;Cluster slot 搬不走單鍵熱點。
08

互動 · maxmemory 撞牆:誰被踢、命中率掉多少

同一份工作集、同一條存取軌跡,只換政策就換一整組犧牲者。這裡把「候選集合是誰」造成的差別算出來。

互動 · maxmemory 與淘汰政策 拖動參數,命中率與犧牲者即時重算

模擬條件:一條 4000 次存取的軌跡,主要依 Zipf 熱度分佈抽 key,另外每 500 次插入一段 150 次的冷 key 掃描(模擬跑在旁邊的批次作業)。下面所有數字都是這條軌跡跑完後的計數。

4000 把
512 B
256 KB
40%
1.1
工作集需求 maxmemory
常駐的無 TTL key(volatile-* 永遠踢不掉) 常駐的有 TTL key(唯一的候選集合) 下方細條:工作集本來需要多少

預設就停在最經典的誤配上:政策是 volatile-lru,但只有 40% 的 key 設了 expire——無 TTL 的那 60% 會慢慢佔滿記憶體且永遠不被選中,於是驅逐與 OOM 同時出現。把 TTL 比例拉到 0,數字會與 noeviction 一模一樣;切到 volatile-ttl,「被踢的是熱鍵嗎」那一列會跳上去,因為它挑的是 TTL 最短的,不是最冷的。另外兩點要誠實說明:真實 Redis 的 LRU/LFU 是 maxmemory-samples 取樣近似,本模擬用的是精確排序,所以實機的誤殺通常比這裡更多;「業務讀取命中率」刻意把掃描批次的存取排除,因為掃描本來就必然 miss,混在一起會蓋掉政策之間的差別。

一句話答法淘汰政策的差別不在演算法名字,在候選集合:allkeys-* 全部可踢,volatile-* 只踢有 expire 的,沒有候選就回 OOM;volatile-ttl 挑的是活得最短的,不是最冷的,所以它會誤殺熱鍵。
09

面試折衷與邊界

先畫執行模型,再決定持久化與拓撲;Cluster 與 Sentinel 不要混成一句「開了高可用」。

面試常問 先答這句 不要滑去
為什麼單執行緒還快? 記憶體 O(1) 命令無鎖、快取親和;慢的是 O(N) 與大 key 把 I/O threads 說成平行執行命令
RDB 還是 AOF? 要時間點備份用 RDB;要崩潰窗口約一秒用 AOF everysec。常兩者都開,復原聽 AOF 「開持久化就不會丟資料」
Sentinel 與 Cluster? Sentinel:同一份 keyspace 的主從切換。Cluster:16384 slot 水平切,客戶端懂 MOVED/ASK 用 hash ring/虛擬節點描述 Cluster
滿記憶體怎麼辦? 先講政策候選集合:allkeys 什麼都踢,volatile 只踢有 TTL 的,noeviction 拒寫 只答「開 LRU」而不說哪些 key 有資格
熱 key 怎麼拆? 同一 key 同一槽,加節點無效;本地快取、讀 replica、或複製成 N 把讀鍵 與快取擊穿的 mutex 回源混為一談

本篇停在 Redis 行程與拓撲。下一步若走訊息骨幹,從 訊息佇列基礎 進;若走 API 契約,先備是 gRPC 與 Protocol Buffers。

關鍵Redis 不是免費的可靠性層

單執行緒給你原子命令,不給你跨 key 的分散式交易;AOF everysec 給你約一秒窗口,不給你零遺失;Sentinel 給你新主,不給你舊主寫入的 fencing;Cluster 給你 slot 遷移,不給你熱槽自動分裂。把這些窗口寫進 SLO,比把 Redis 當「不會壞的記憶體」安全。

一句話答法Redis 的面試分數來自分清四層:命令複雜度、持久化窗口、Sentinel 對 Cluster、以及 eviction 的候選集合。每一層都有明確做不到的事。