快取三大災難:穿透、擊穿、雪崩,與正確解法
· tech
📑 目錄
把 Redis 當快取,最經典的模式是 cache-aside(旁路快取):讀取先查 cache,命中就回傳、沒命中(miss)才查資料庫,再把結果回填進 cache。平常運作得很好——直到某些情況下,大量請求繞過 cache、直接打爆後面的資料庫。這類破口有三個著名的名字:穿透、擊穿、雪崩。它們念起來很像、常被搞混,但其實是三種不同的失敗,各有各的解法。
三兄弟長不一樣:穿透 / 擊穿 / 雪崩
先把三者的差別講清楚——搞懂它們「破在哪」,解法就不會亂套:
對症下藥:三種破口,三種補法
差別搞懂了,解法就很自然——每一種破口,都對應一種「讓失敗別集中」的手法:
三種解法的手法不同,但骨子裡是同一句話:別讓失敗集中在同一個時間、同一個點。 空值快取是「把打不到的擋在門外」、互斥鎖是「把並行收斂成一個」、隨機 TTL 是「把同時發生的打散開」——擋住、收斂、打散,對應三種集中的破口。至於擊穿要用的互斥鎖,牽涉到「怎麼在分散式下正確地搶一把鎖」,那本身是個大題目,留到下一篇的分散式鎖細講。
落成命令:三種補法怎麼下
三災難的解法,落成 redis-cli 就這幾條:
# cache-aside 讀:先查快取,miss 才回填(一定要帶 TTL)
GET article:42 # miss → 打 DB 撈,再回填 ↓
SET article:42 "<json>" EX 300 # 存 5 分鐘;TTL 加點隨機 → 防雪崩同時到期
# 擊穿:互斥鎖,只放「一個」請求去重建熱點
SET lock:article:42 1 NX EX 10 # 搶到(回 OK)才查 DB,其餘等一下重試
# 穿透:對「查無」也快取一個空值(短 TTL),擋住重複打 DB
SET article:404 "" EX 30
三條命令對三種破口:回填帶隨機化 TTL 打散雪崩、SET NX 當互斥鎖收斂擊穿、空值短 TTL 擋穿透。共通的一句話是——別讓大量 miss 在同一刻砸向 DB。
反思
三個名字很像,但本質是三種不同的集中
穿透、擊穿、雪崩,我剛學時被這三個中文名搞得暈頭轉向——它們聽起來就像同一件事的三種說法。真正把它們釐清,是我意識到差別只在一個維度:「打到 DB 的請求,為什麼集中?」 穿透是因為查的東西根本不存在、cache 這道牆天生擋不住(空間上的漏);擊穿是因為一個熱點在過期瞬間、並行全撞上來(時間上的一個點);雪崩是因為一大片 key 剛好同時到期(時間上的一大片)。把它拆成「不存在 / 一個點 / 一大片」,名字就不再需要死背,而是能從情境自己推出來。遇到一組容易混淆的術語,找到那個能區分它們的維度,比背定義有用一百倍。
這些解法的共通精神:消除「同步性」
寫這篇最大的收穫,是發現三種解法其實在解同一個更深的問題——同步性(synchronization)是災難的放大器。一萬個請求如果錯開,DB 輕鬆消化;但如果它們同時發生(同時過期、同時撲向一個熱點),就會瞬間壓垮系統。所以隨機 TTL 的 jitter、互斥鎖的收斂,本質都是在打破這種致命的同步。這個模式我後來到處都看得到:排程加抖動避免午夜驚群、重試加隨機退避(backoff)避免 retry storm、啟動時錯開暖機避免驚群——只要看到「大量東西同時做同一件事」,就要警覺它可能是下一場災難的引信。把同步的尖峰抹平成分散的緩坡,是可靠度工程裡反覆出現的一招。
快取的難,不在快取本身,在「miss 的那一刻」
用 Redis 當快取,大家的注意力都放在「命中時多快」,但真正會出事的,永遠是 miss 的那一刻——cache 沒擋住,壓力瞬間傳導到後面的資料庫。這三大災難,全部都發生在 miss 之後。所以我現在設計任何快取層,第一個想的不是「命中率多高」,而是「當它 miss、當它整個掛掉,後面的 DB 扛得住嗎?」 一個沒想清楚 miss 行為的快取,平常幫你擋住流量、歲月靜好,但它同時也養大了後面 DB 承受不起的真實流量——一旦快取失守,那些流量就會原形畢露、瞬間壓垮 DB(連鎖失效)。快取是加速器,但別忘了它也是一道你遲早要面對「失守會怎樣」的防線。