緩存系統一定程度上極大提升系統併發能力,但同樣也增加系統的複雜度,下面針對緩存系統設計與使用中面臨的常見問題展開。主要是針對熱門問題:緩存穿透/熱點數據集中失效/雪崩/一致性問題的總結及解決方案。
There are only two hard things in Computer Science: cache invalidation and naming things. – Phil Karlton
計算機科學中有兩件難事:緩存失效和命名
緩存穿透
什麼是緩存穿透?
緩存穿透是指訪問不存在數據,從而繞過緩存,直取數據源(大量數據源讀取操作)
穿透帶來的問題?
大量的請求到數據庫去查詢,可能會導致你的數據庫由於壓力過大而宕掉。
解決辦法?
1.緩存空值
思路:緩存中沒有存儲這些空數據的key,導致每次查詢都到數據庫去了,那麼我們就可以爲這些key對應的值設置爲null 丟到緩存裏面去,後面再出現查詢這個key 的請求的時候,直接返回null 。
2. BloomFilter
BloomFilter 類似於一個hbase set 用來判斷某個元素(key)是否存在於某個集合中。
這種方式在大數據場景應用比較多,比如 Hbase 中使用它去判斷數據是否在磁盤上,還有在爬蟲場景判斷url 是否已經被爬取過。
這種方案可以加在第一種方案中,在緩存之前在加一層 BloomFilter (在查詢的時候先去 BloomFilter 去查詢 key 是否存在,如果不存在就直接返回,存在再走查緩存 -> 查 DB)。
小結
針對於一些惡意攻擊,攻擊帶過來的大量key 是不存在的,那麼我們採用第一種方案就會緩存大量不存在key的數據。此時我們採用第一種方案就不合適了,我們完全可以先對使用第二種方案進行過濾掉這些key。
針對這種key異常多、請求重複率比較低的數據,我們就沒有必要進行緩存,使用第二種方案直接過濾掉。
而對於空數據的key有限的,重複率比較高的,我們則可以採用第一種方式進行緩存。
緩存擊穿(熱點數據集中失效)
什麼是擊穿(熱點數據集中失效)?
在平常高併發的系統中,大量的請求同時查詢一個 key 時,此時這個key正好失效了,就會導致大量的請求都打到數據庫上面去。這種現象我們稱爲緩存擊穿。
會帶來什麼問題?
會造成某一時刻數據庫請求量過大,壓力劇增。
如何解決?
1. 設置不同的失效時間
爲了避免這些熱點的數據集中失效,那麼我們在設置緩存過期時間的時候,我們讓他們失效的時間錯開。比如在一個基礎的時間上加上或者減去一個範圍內的隨機值。
2. 互斥鎖
多個線程同時去查詢數據庫的這條數據,那麼我們可以在第一個查詢數據的請求上使用一個 互斥鎖來鎖住它。其他的線程走到這一步拿不到鎖就等着,等第一個線程查詢到了數據,然後做緩存。
緩存雪崩
什麼是緩存雪崩?
緩存雪崩是指緩存系統失效,導致大量請求同時進行數據回源,導致數據源壓力驟增而崩潰。兩種情況會導致此問題:
- 多個緩存數據同時失效
- 緩存系統崩潰
解決辦法?
事前:使用集羣緩存,保證緩存服務的高可用,這種方案就是在發生雪崩前對緩存集羣實現高可用,如果是使用 Redis,可以使用 主從+哨兵 ,Redis Cluster 來避免 Redis 全盤崩潰的情況。
事中:ehcache本地緩存 + Hystrix限流&降級,避免MySQL被打死,使用 ehcache 本地緩存的目的也是考慮在 Redis Cluster 完全不可用的時候,ehcache 本地緩存還能夠支撐一陣。使用 Hystrix進行限流 & 降級 ,比如一秒來了5000個請求,我們可以設置假設只能有一秒 2000個請求能通過這個組件,那麼其他剩餘的 3000 請求就會走限流邏輯。
事後:開啓Redis持久化機制,儘快恢復緩存集羣,一旦重啓,就能從磁盤上自動加載數據恢復內存中的數據。
緩存更新與數據一致性
常見緩存更新策略:
- 讀操作:命中緩存則返回,無緩存則取回源數據,寫緩存
- 寫操作:先刪除緩存,再更新數據源
會帶來什麼問題?
讀寫併發的場景下先刪緩存操作可能導致髒數據入緩存。比如:
- 線程A 寫操作:刪除緩存
- 線程B 讀操作:無緩存則取回源數據(舊數據),回寫緩存(此時緩存中爲舊數據)
- 線程A 寫操作:更新數據源
- 此時緩存數據不一致:緩存中爲舊數據,數據源爲新數據,出現緩存舊數據問題
解決辦法?
1. Cache Aside Pattern:先數據源更新後,再失效緩存(由等待下次讀取來回寫緩存)
- 優勢:無緩存舊數據問題、緩存系統維護簡單、Facebook推薦方案
- 問題:無法絕對杜絕併發讀寫問題,這種問題出現概率極低,幾點要求:緩存已過期、併發讀寫、讀數據比寫數據快、但讀操作更新緩存比寫操作失效緩存慢(也就是說寫操作的行爲需完全發生在讀操作兩步之間),一般而言讀操作(讀庫+更新緩存)時長要小於寫操作(更新數據源+失效緩存),所以認爲這種併發問題概率較低。可以通過增加鎖機制,解決併發問題。
2. Read Through Pattern:更新數據源由緩存系統操作
讀取數據
時,如緩存失效,則緩存服務取回源數據更新緩存- 而Cache Aside中是由應用服務(調用方)更新緩存
- 這套對調用方是透明的,只有一套存儲系統,而無視緩存、數據源的差異
3. Write Through Pattern:更新數據源由緩存系統操作
寫數據
時,如緩存失效,則直接更新數據源(不做任何緩存操作);如命中緩存,則更新緩存(由緩存系統更新數據源)- 在緩存失效下寫操作的處理後,何時更新緩存呢?下一次讀操作,按
Read Through
中緩存失效策略來更新緩存
4. Write Behind Caching Pattern:又稱Write Back
一句話總結:更新數據時,只更新緩存,不更新數據源(緩存異步批量
更新數據源)
優勢:
- 更新緩存爲內存操作,讀寫I/O非常高
- 異步批量更新數據源,合併多個操作
問題:
緩存不滿足強一致性要求
強一致性和高性能的衝突
、高可用和高性能的衝突
終究會使Trade-Off- 實現複雜,需跟蹤哪些Cache更新,成本較高
一般而言,推薦Cache Aside Pattern
方案,容忍較小概率的不一致(同時也可以增加鎖機制解決此低概率併發問題),簡化緩存系統複雜度。