快取有一個殘酷的特性:它撐得越好,大家越依賴它,它倒下的那一刻摔得越重。
想像你的首頁主打商品,快取命中率長期 99%。DB 平常很閒——閒到你的容量規劃是按「有快取」的世界做的。然後某一秒,這個熱門 key 的 TTL 到了。就在它過期到「第一個人把快取填回來」之間的這段空窗,湧進來的每一個請求都 cache miss、每一個都衝去打 DB。DB 瞬間吃到平常幾十倍的流量。
這就是 cache stampede。它不是理論——我把這個場景寫成一個可跑的小實驗:讓熱門 key 過期,然後放 10 個併發請求進來。天真版的 cache-aside,DB 被打了 10 次;同樣 10 個請求,加上 lock-on-miss 之後,DB 只被打 1 次。差距就是這麼直接。
真實環境的版本我繳過學費——02 的翻車帳單那次複合事故裡,其中一個洞就是多處快取的 TTL 訂在重複的時間點,同一瞬間集體過期,本來就被爬蟲壓著的 DB 又吃下一波集中重算,直接炸掉。當時我還叫不出 stampede 這個名字。回頭看最不甘心的是:jitter 一行隨機數就能擋掉那波集中過期——知道病名之後,藥其實都是現成的,貴的是知道病名的那一次。
先把三個詞分清楚
中文圈這幾個詞常常混著用,但它們是三種不同的病,藥也不同:
擊穿——「一個」熱門 key 過期,瞬間的併發全部 miss。病灶是單一 key 的空窗期。
雪崩——「一大批」key 同時到期。典型成因很好笑也很真實:某次上版把一萬個商品快取一次寫進去,全部用同一個 TTL 300 秒——五分鐘後它們同一秒集體過期,miss 海嘯直接淹過去。
穿透——查詢「根本不存在」的資料。快取裡永遠不會有、DB 也查不到,所以每一次都打穿到 DB。惡意掃描不存在的 id 就是這招(解法是空值快取或 bloom filter,這篇先不展開)。
擊穿和雪崩的共同本質是「過期的瞬間沒有防護」,下面兩個解法各治一種。
解法一:lock-on-miss——只讓一個人去查 DB
擊穿的問題在於:10 個請求同時發現 cache miss,10 個都覺得「該我去查 DB 了」。但明明一個人去查就夠了。
所以 miss 的時候先搶一把短鎖(Redis 的 SET key NX EX 就夠用):
- 搶到的人:去查 DB、把結果填回快取、釋放鎖。
- 沒搶到的人:不打 DB,短暫等一下、重讀快取——贏家填好了,大家直接命中。
這招也叫 singleflight:同一時刻對同一個 key 的重複查詢,被壓縮成一趟。前面實驗裡「10 次變 1 次」就是它的功勞。
兩個實作上一定要顧的細節:
- 鎖要有 TTL。 搶到鎖的人可能查到一半掛掉,鎖沒有過期時間的話,其他人會永遠等一個不存在的贏家。
- 等待要有底線。 等了幾輪還是沒等到快取被填好,就自己去查 DB 當保險——寧可多打一次 DB,也不要讓請求掛在那裡。
解法二:TTL 加 jitter——別讓大家同一秒死
雪崩的成因是「同時寫入、同一個 TTL」,解法便宜到不可思議:每個 key 的 TTL 加一點隨機值。
與其全部 300 秒,不如 300 加上隨機 0 到 60 秒。一萬個 key 的過期時間就從「同一秒」被打散成「一分鐘內陸續」,每一秒只有一小撮 key 過期,DB 完全消化得掉。
一行隨機數,換掉一次可能的事故。這大概是整個快取領域投資報酬率最高的一行 code。
還有一層要想:fail-open 的反噬
Stampede 講到底是「DB 突然接手它平常不用扛的流量」。同一個問題還有一個更陰的變形:你的程式為了韌性設計了 fail-open——Redis 掛了或逾時,就直接去打 DB。方向完全正確,但如果 Redis 掛的是「整台」,那 fail-open 等於把原本 99% 由快取扛的流量整批倒進 DB。
所以防護要成對出現:fail-open 讓服務活著,但 DB 前面還是要有限流或斷路器兜底,不然只是把死法從「快取掛」換成「DB 被打死」。容量規劃時記得問一句:如果快取整層消失,DB 扛得住幾成流量? 答案通常很難看,但先知道總比出事才知道好。
回到開頭那句話:快取撐得越好,倒下越痛。所以熱門 key 用 lock-on-miss 保護過期空窗、批量寫入的 TTL 加 jitter 打散到期時間、fail-open 後面補限流——三件事都不難,難的是在還沒出事的時候就把它們做進去。
(cache 該怎麼失效、TTL 策略怎麼訂,見同系列的 invalidation 篇;Redis 本身卡住時服務怎麼自保,之後在韌性主題展開。)
接下來往哪走
- TTL 策略:快取的過期時間不是隨手填的魔法數字 — jitter 只是 TTL 設計的其中一步,整套訂法在這篇
- 為什麼「加個 cache」常常讓事情更糟 — stampede 是四種翻車之二,另外三種也認一認
- 四種 Cache Pattern — lock-on-miss 守讀路徑,寫路徑的髒窗口這篇管