結論先講

Cache 的效果取決於你加在哪裡。 讀取密集場景 +6.5 倍(第 21 篇),但 Auth 場景(bcrypt)只有 +5%。加 cache 之前先做壓測找到瓶頸,然後只 cache 瓶頸路徑。Cache 不是撒胡椒粉——不是撒越多越好。


什麼該 cache、什麼不該

該 cache

場景原因預期提升
讀取頻繁、很少變動商品列表、文章內容6.5 倍(壓測數據)
計算成本高排行榜、統計報表高(省掉 DB 聚合查詢)
外部 API 回應匯率、天氣、第三方資料中(省掉 HTTP roundtrip)

不該 cache

場景原因
寫入為主的操作Cache 馬上就髒了,invalidation 成本 > cache 效益
bcrypt / CPU-boundCache 不了 CPU 計算(第 6 篇
用戶個人資料(高度個人化)每個用戶的 cache 不同,hit rate 低
即時性要求極高股票報價、即時庫存(cache 延遲不可接受)

壓測數據的佐證

Cache 目標提升結論
DB 讀取(Post List)+6.5 倍值得 cache
標準 CRUD(50:50 讀寫)+25%有效果但不驚豔
Auth(bcrypt)+< 5%不值得——瓶頸是 CPU 不是 DB

Cache-Aside Pattern:最常用也最安全

讀取:
  1. 查 Redis → 有就直接回(cache hit)
  2. Redis 沒有 → 查 DB → 寫入 Redis → 回(cache miss)

寫入:
  1. 寫 DB
  2. 刪 Redis 中的對應 key(不是更新,是刪除)

為什麼寫入時「刪除」不是「更新」

如果兩個請求同時更新同一筆資料:

時間軸:
  Request A: 更新 DB(name=Alice)
  Request B: 更新 DB(name=Bob)
  Request A: 更新 Cache(name=Alice)← 比較慢
  Request B: 更新 Cache(name=Bob)← 比較快

結果: DB 是 Bob(正確),Cache 是 Alice(錯誤!)

如果用「刪除」:

  Request A: 更新 DB(name=Alice)→ 刪 Cache
  Request B: 更新 DB(name=Bob)→ 刪 Cache
  下次讀取: Cache miss → 從 DB 拿 Bob → 寫入 Cache

結果: DB 和 Cache 都是 Bob(正確)

刪除永遠不會出錯。 最壞情況是多一次 cache miss,不會出現資料不一致。


Cache Invalidation 三種策略

策略做法適合風險
TTL(到期時間)SET key value EX 300大部分場景過期前可能拿到舊資料
主動刪除寫入 DB 後刪 cache key一致性要求高漏刪就出 bug
事件驅動DB 變更 → 發 event → 刪 cache微服務最複雜

TTL 怎麼設

資料類型建議 TTL理由
商品列表5-15 分鐘偶爾更新,可以稍微過期
用戶 session30-60 分鐘安全考量
統計報表1-24 小時不需要即時
匯率/天氣5-30 分鐘取決於資料來源更新頻率

沒有「正確的 TTL」——只有「你的業務能接受多少秒的過期資料」。


Cache Stampede:最危險的 cache 問題

什麼是 stampede

一個熱門的 cache key 過期了。在同一毫秒內,1000 個請求同時發現 cache miss → 1000 個請求同時查 DB → DB 被打爆。

正常: 999 次 cache hit + 1 次 cache miss + 1 次 DB 查詢
Stampede: 0 次 cache hit + 1000 次 cache miss + 1000 次 DB 查詢

防禦方法

1. Mutex Lock(最常用)

async function getWithLock(key) {
  let value = await redis.get(key);
  if (value) return JSON.parse(value);
 
  // 嘗試拿鎖
  const lock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 5);
  if (lock) {
    // 拿到鎖:查 DB、寫 cache
    value = await db.query(...);
    await redis.set(key, JSON.stringify(value), 'EX', 300);
    await redis.del(`lock:${key}`);
    return value;
  } else {
    // 沒拿到鎖:等一下再試
    await sleep(50);
    return getWithLock(key);
  }
}

2. 提前續期(Background Refresh)

在 TTL 剩 20% 時,背景自動更新 cache,不等到真正過期。

3. 永不過期 + 主動更新

不設 TTL,寫入 DB 時主動更新 cache。適合變更頻率可控的資料。


微服務場景的 Cache 特殊問題

跨服務的 cache invalidation

Service A 更新了商品價格,Service B 的 cache 裡還是舊價格。

解法:

  1. 共用 Redis:所有 service 連同一個 Redis,A 刪 key,B 下次讀就是新的
  2. Event-Driven:A 發 event「price.updated」,B 收到後刪自己的 cache
  3. 短 TTL:不做主動 invalidation,靠 TTL 自動過期(最簡單但最不即時)

每個 service 要不要自己的 Redis

做法優點缺點
共用 Redis簡單、跨服務 invalidation 容易一掛全掛
各自 Redis隔離性好invalidation 要靠 event

小規模(< 10 個 service)共用 Redis 就好。大規模再考慮分開。


下一篇

Cache 的常見誤用:你可能正在犯的五個錯 — 把整個 DB table 塞進 Redis、cache key 沒有 namespace 導致衝突、忘記 cache warmup 導致部署後 DB 被打爆、cache 比 DB 還大。


本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:從 MySQL 遷移到 PostgreSQL:實戰踩坑指南 → 下一篇:Cache 的常見誤用:你可能正在犯的五個錯