架構與思維:一次緩存雪崩的災難復盤
1 真實案例
雲辦公系統用戶實時信息查詢功能優化發佈之後,系統發生宕機事件(系統掛起,頁面無法加載)。
1.1 背景
1.2 問題處理
2 緩存雪崩
2.1 概念
緩存雪崩是指大量的key設置了相同的過期時間,導致在緩存在同一時刻全部失效,造成瞬時DB請求量大、壓力驟增,引起雪崩。
2.2 解決方案分析
2.2.1 緩存集群+數據庫集群
在系統容量設計的時候,應該能夠預見後期會有大量的請求,所以在發生雪崩前對緩存集群實現高可用,如果是使用 Redis,可以使用 主從+哨兵 ,Redis Cluster 來避免 Redis 全盤崩潰的情況。
同樣的,也需要對數據庫進行高可用保障,因為透過緩存之後,真正考驗的是數據庫的抗壓能力。所以 1主N從 甚至 數據庫集群 是我們需要重點去考慮的。
2.2.2 適當的限流、降級
可以使用 Hystrix進行限流 + 降級 ,比如像上面那種情況,一下子來了1W個請求,不是當前系統的吞吐能力能夠承受的,假設單秒TPS的能力只能是 5000個,那麼剩餘的 5000 請求就可以走限流邏輯。
可以設置一些默認值,然後調用我們自己降級邏輯去FallBack,保護最後的 MySQL 不會被大量的請求掛起。 除了Hystrix之外,阿里的Sentinel 和 Google的RateLimiter 都是不錯的選擇。
Sentinel 漏桶算法

RateLimiter 令牌桶算法
另外可以考慮使用用本地緩存來進行緩衝,在 Redis Cluster 不可用的時候,不至於全線崩潰。
2.2.3 隨機過期時間
可以給緩存設置過期時間時加上一個隨機值時間,使得每個key的過期時間分佈開來,不會集中在同一時刻失效。
隨機值我們團隊的做法是:n * 3/4 + n * random() 。所以,比如你原本計劃對一個緩存建立的過期時間為8小時,那就是6小時 + 0~2小時的隨機值。
2.2.4 緩存預熱

3 緩存穿透
3.1 概念
比如 我們查詢用戶的信息,程序會根據用戶的編號去緩存中檢索,如果找不到,再到數據庫中搜索。如果你給了一個不存在的編號:XXXXXXXX,那麼每次都比對不到,就透過緩存進入數據庫。
這樣風險很大,如果因為某些原因導致大量不存在的編號被查詢,甚至被惡意偽造編號進行攻擊,那將是災難。
3.2 解決方案分析
3.2.1 緩存空值
發生穿透的原因是緩存中沒有存儲這些空數據的key,或者壓根這個數據的key是不會存在的,從而導致每次查詢都進入數據庫中。
我們就可以將這些key的值設置為null,並寫到緩存池中。後面再出現查詢這個key 的請求的時候,直接返回null,這樣就在緩存池中就被判斷返回了,壓力在緩存層中,不會轉移到數據庫上。
3.2.2 BloomFilter
我們稱作布隆過濾器,BloomFilter 類似於一個hbase set 用來判斷某個元素(key)是否存在於某個集合中。
這種方式在大數據場景應用比較多,比如 Hbase 中使用它去判斷數據是否在磁盤上。還有在爬蟲場景判斷url 是否已經被爬取過。
這種方案可以加在第一種方案中,在緩存之前在加一層 BloomFilter ,把存在的key記錄在BloomFilter中,在查詢的時候先去 BloomFilter 去查詢 key 是否存在,如果不存在就直接返回,存在再走查緩存 ,投入數據庫去查詢,這樣減輕了數據庫的壓力。
流程圖如下:

3.2.3 兩種方案的選擇判斷
前面說過,可能會存在一些惡意攻擊,偽造出大量不存在的key ,這種情況下如果我們如果採用緩存空值的辦法,就會產生大量不存在key的null數據。顯然是不合適的,這時我們完全可以使用第二種方案進行過濾掉這些key。
所以,判斷的依據是:
針對key非常多、請求重複率比較低的數據,我們就沒有必要進行緩存,使用 BloomFilter 直接過濾掉。
而對於空數據的key有限的,重複率比較高的,我們則可以採用 緩存空值的辦法 進行處理。
4 緩存擊穿
4.1 概念
4.2 解決方案
4.2.1 鎖的方式
這種現象是多個線程同時去查詢數據庫的這條數據,那麼我們可以在第一個查詢數據的請求上使用一個 互斥鎖來鎖住它。
其他的線程走到這一步拿不到鎖就等着,等第一個線程查詢到了數據,然後做緩存。後面的線程進來發現已經有緩存了,就直接走緩存。
鎖不好的地方就是在其他線程在拿不到鎖的時候就等待,這個會造成系統整體吞吐量降低,用戶體驗度也不好。
4.2.2 空初始值
這是一種短暫降級的方式:
如果一個緩存失效的時候,有無數個請求狂奔而來,而第一個請求從進入緩存池,判空,再到數據庫檢索,再查詢出結果並返回設置緩存的這個過程里,緩存是不存在的。
這個就很危險,超高並發下這個短暫的過程足已讓千千萬萬請求投向數據庫。更別提這可能是個慢查詢,整個過程可能長達2s以上,那對數據庫是一種非常大的傷害。
業內有一種做法叫做空初始值,短暫的局部降級來保證整個數據庫系統不被擊穿。大概流程如下:
可以看出,整個過程中我們犧牲了A、B、C、D的請求,他們拿回了一個空值或者默認值,但是這局部的降級卻保證整個數據庫系統不被擁堵的請求擊穿。
這也是我面試中最喜歡問候選人的緩存類問題。

