「Computer Science 中只有兩件難事:快取失效(Cache Invalidation)和命名。」
— Phil Karlton,廣為流傳的名言
這個笑話之所以成名,是因為它說出了真相:快取很容易加,難的是讓快取裡的資料保持正確。
為什麼要快取?速度差距有多大?
快取的本質是「用空間換時間」:把計算代價高或存取慢的資料,存在更快的地方。 先感受一下各層儲存的速度差距:
數量級差距的實際影響
Redis 讀取(~1ms)vs DB 無索引查詢(~100ms)差 100 倍。 如果你的 API 每次請求都打 DB,加了 Redis 後同樣的硬體可以多撐 100 倍的流量。 這不是優化,是量級的改變。
Redis 常用資料結構
Redis 不只是 key-value store,它有多種資料結構,選對結構能大幅簡化實作。
String vs Hash:最常被問到的選擇題
用 String(JSON 序列化)
適合:整個物件作為一個整體讀取,不需要部分更新
SET user:123 '{"name":"J","age":28}' EX 3600用 Hash(欄位分開存)
適合:只更新其中一個欄位,不想序列化整個物件
HSET user:123 name "J" age 28⚡ 實務上,String + JSON 更普遍:程式語言都有 JSON 序列化,且通常整筆讀取。Hash 適合欄位數量多且頻繁部分更新的場景(如用戶設定頁)。
String
SET / GET / INCR / EXPIRE最基本的快取(JSON 字串化)、計數器(INCR)、Session Token
Hash
HSET / HGET / HGETALL用戶資料(欄位可部分更新)、避免每次序列化整個物件
List
LPUSH / RPUSH / LRANGE / LPOP訊息佇列(Message Queue)、最近瀏覽記錄(取前 N 筆)
Set
SADD / SISMEMBER / SUNION去重(唯一訪客)、標籤系統、好友關係、點讚狀態
Sorted Set(ZSet)
ZADD / ZRANGE / ZRANGEBYSCORE排行榜(分數排序)、延遲隊列(timestamp 排序)
Bitmap / HyperLogLog
SETBIT / PFADD / PFCOUNTBitmap:簽到記錄(1位=1天);HLL:大規模 UV 統計(允許誤差)
快取策略:四種模式
Cache-aside(Lazy Loading)
應用程式直接管理快取:讀時先查 Cache,沒有才查 DB 並回寫 Cache(稱為 Cache Miss 後填入)。 寫時只寫 DB,快取讓它自然過期(TTL)或主動刪除。
✅ 優點
只快取真正被讀取的資料;DB 故障時快取仍可服務部分請求
⚠️ 缺點
Cache Miss 時需要 3 次操作(讀 Cache + 讀 DB + 寫 Cache),首次較慢
Write-through
每次寫入時,同時寫 DB 和 Cache。保持 Cache 與 DB 強一致,但寫入延遲較高。
✅ 適合
寫後馬上讀的場景;對快取一致性要求高的資料
⚠️ 缺點
寫入延遲翻倍;寫多讀少時快取中大量資料從未被讀取(浪費記憶體)
Write-behind(Write-back)
寫入只寫 Cache,非同步批次寫入 DB。寫入延遲最低, 適合高頻寫入的場景(遊戲積分、實時計數器)。代價是快取故障時資料可能遺失。
風險提示
Redis Crash 且 AOF/RDB 未開啟的情況下,尚未落地 DB 的資料會永久遺失。 需要搭配 Redis Persistence 機制(AOF appendfsync everysec)與監控告警。 金融、訂單等關鍵資料不應使用此策略。
Read-through
與 Cache-aside 流程相同,但由快取庫(而非應用程式)自動處理 Cache Miss 時的 DB 查詢。 應用程式只與快取層互動,不直接調用 DB。 適合使用支援 Read-through 的快取中間件(如 Caffeine + 自定義 CacheLoader)。
快取策略選擇指南
| 場景 | 推薦策略 | 原因 |
|---|---|---|
| 讀多寫少(用戶資料、商品詳情) | Cache-aside | 只快取實際被讀的資料,省記憶體 |
| 寫後立即讀(下單後看訂單) | Write-through | 保持一致性,避免讀到舊資料 |
| 超高頻寫(遊戲積分、點擊計數) | Write-behind | 批次落地 DB,降低寫入壓力 |
| 需要複雜 Query 結果(JOIN、聚合) | Cache-aside(手動 Key 設計) | 把計算代價高的結果整體快取 |
快取失效(Cache Invalidation)策略
快取失效是最難的部分。有兩種思路:被動失效(TTL 到期)和主動失效(資料更新時刪除)。 兩者各有適用場景。
被動失效:TTL 過期
設定快取存活時間,時間到就自動失效。簡單、不需要程式邏輯, 但在 TTL 內資料可能是舊的。
熱門商品:TTL = 5 分鐘
用戶基本資料:TTL = 1 小時
靜態設定檔:TTL = 24 小時
Session Token:TTL = 15 分鐘
主動失效:事件驅動刪除
資料更新時,主動刪除對應的快取 Key,讓下次讀取重新從 DB 載入最新資料。
三個必須了解的快取問題
快取設計有三個典型的陷阱,都源自「Cache 與 DB 之間的資料斷層」。 理解它們的根因,面試和實作時才能給出正確的解法而不是頭痛醫頭。
快取穿透(Cache Penetration)
大量請求查詢一個「確定不存在於 DB」的 Key,每次都 Cache Miss → 打穿 DB。常見於惡意攻擊。
快取擊穿(Cache Breakdown)
一個「熱點 Key」快取剛好過期(TTL = 0),此時大量請求同時 Cache Miss 並打到 DB,造成 DB 瞬間壓力。
快取雪崩(Cache Avalanche)
大量 Key 同時過期(例如系統啟動時統一設置了相同 TTL),造成瞬間大量 DB 請求。
生產環境 Redis 最佳實踐
Key 命名規範
使用命名空間前綴避免衝突: user:123 post:456:comments leaderboard:2026-05 方便 SCAN 按模式批次操作
記憶體淘汰策略
⚠️ 預設值是 noeviction(記憶體滿了直接報 OOM 錯誤)! 生產環境必須顯式設定: maxmemory-policy allkeys-lru # 推薦 allkeys-lru:淘汰最久未用的(通用快取場景) volatile-lru:只淘汰有 TTL 的 Key noeviction:滿了就報錯(預設,不適合快取)
Pipeline / MULTI-EXEC
批次操作用 Pipeline 減少 RTT: const pipe = redis.pipeline(); pipe.set(...); pipe.expire(...); await pipe.exec(); 比逐一操作快 5-10 倍
監控指標
關注以下指標: · Cache Hit Rate(>90% 才有意義) · Memory Usage(<80% 可用) · Evicted Keys(非 0 表示記憶體不足) · Latency(p99 <5ms)
重點整理
Cache-aside 是預設選擇
讀多寫少的場景幾乎都適用。Cache Miss 時查 DB 並回寫,更新時刪除快取讓 TTL 自然重建。
TTL 加 Jitter 防雪崩
不要統一設置相同的 TTL,加入 ±10% 的隨機值,讓過期時間分散,避免瞬間大量 DB 請求。
空值也要快取
防穿透攻擊:查詢不存在的 Key 也要快取 null(設短 TTL),否則每次都穿透到 DB。
Cache Hit Rate 是核心指標
Hit Rate < 90% 代表快取設計有問題(TTL 太短、Key 設計不合理)。先量後優化。
快取不是銀彈
快取的代價是複雜度:你有兩份資料,要保持一致。一致性需求高的場景(餘額、庫存)要謹慎使用。
面試三問必答
問你快取時,說清楚:① 用哪種策略?② TTL 設多長?③ 如何處理穿透、擊穿、雪崩?
上一篇
EP.02 負載均衡