文章目錄
- 緩存雪崩、擊穿、穿透
- 1.緩存雪崩
- 造成緩存雪崩
- 解決緩存雪崩
- 2. 緩存擊穿
- 造成緩存擊穿
- 解決緩存擊穿
- 3.緩存穿透
- 造成緩存穿透
- 解決緩存穿透
- 更新數據時,如何保證數據庫和緩存的一致性?
- 1. 先更新數據庫?先更新緩存?
- 解決方案
- 2. Cache Aside策略
- ① 先更新數據庫,再刪除緩存
- 保證更新數據庫、刪除緩存都執行成功
- ② 先刪除緩存,再更新數據庫
- 解決方案

緩存雪崩、擊穿、穿透
一般用戶數據存儲于磁盤,讀寫速度慢。
使用redis作為緩存,相當于數據緩存在內存,大大提高系統性能
redis作為緩存,就會有緩存異常的三個問題
1.緩存雪崩
緩存都設置了過期時間
造成緩存雪崩
-
大量緩存數據在同一時間過期
-
redis故障宕機
若此時有大量用戶請求,無法在redis處理,都直接訪問數據庫 => 數據庫壓力驟增(嚴重造成數據庫宕機) => 形成一系列連鎖反應 => 整個系統崩潰
解決緩存雪崩
=> 大量緩存數據在同一時間過期時:
-
均勻設置過期時間(對緩存數據的過期時間加上隨機數,保證數據不會在同一時間過期)
-
互斥鎖(當業務線程在處理用戶請求時,如果發現訪問的數據不在redis里,加互斥鎖,保證同一時間內只有一個請求來構建緩存(從數據庫讀取數據,再將數據更新到redis),當緩存構建完成后,再釋放鎖。)
注:互斥鎖設置超時時間,否則若出現請求發生意外阻塞,導致其他請求也一直拿不到鎖 -
后臺更新緩存(讓緩存“永久有效”,將更新緩存的工作交由后臺線程定時更新)
當系統內存緊張時,有些緩存數據被“淘汰”,在“淘汰”和下次更新時間內,業務線程讀取失敗就以為是數據丟失,解決方法:-
后臺線程負責定時更新緩存,同時頻繁地檢測緩存是否失效,若失效,可進行構建緩存
? 檢測時間間隔不能太長,太長導致用戶獲取的數據是空值而不是真正的數據,檢測時間間隔最好是毫秒級,用戶體驗一般
-
業務線程發現緩存數據失效后,通過消息隊列發送一條消息通知后臺線程更新緩存。后臺線程收到消息后,更新前判斷緩存是否存在,不存在則進行構建緩存。
? 緩存更新及時,用戶體驗好
**注:**后臺更新緩存機制適合進行緩存預熱(業務剛上線時,提前緩存數據,不是等待用戶訪問才來觸發緩存構建)
-
=> Redis故障宕機時:
-
服務熔斷或請求限流機制
? 服務熔斷:暫停業務應用對緩存服務的訪問,直接返回錯誤,不再繼續訪問數據庫,直到redis恢復正常。
? 請求限流機制:只將少部分請求發送到數據庫進行處理,再多的請求就在入口直接拒絕服務,等到Redis恢復正常 并把緩存預熱完后。
-
構建redis緩存高可靠集群
? 通過主從節點的方式構建,若redis緩存的主節點宕機,從節點可以切換成為主節點,繼續提供緩存服務
2. 緩存擊穿
造成緩存擊穿
被頻繁訪問的熱點數據過期,此時大量的請求訪問該熱點數據,直接訪問數據庫,數據庫很容易被高并發的請求沖垮
緩存擊穿可以認為是緩存雪崩的一個子集(對應于大量緩存數據在同一時間過期)
解決緩存擊穿
- 互斥鎖
- 不給熱點數據設置過期時間,由后臺異步更新緩存 / 在熱點數據準備過期前,提前通知后臺線程更新緩存以及重新設置過期時間
3.緩存穿透
對于緩存雪崩、擊穿,數據仍然在數據庫,一旦緩存恢復相應的數據,就可以減輕數據庫的壓力
而對于緩存穿透:
? 用戶訪問的數據,既不在緩存中,也不在數據庫中,導致請求在訪問緩存時,發現緩存缺失,再去訪問數據庫,發現數據庫也沒有要訪問的數據,沒辦法構建緩存來服務后續請求。當有大量的這樣的請求時,數據庫的壓力驟增
造成緩存穿透
- 業務誤操作,緩存中數據和數據庫數據都被誤刪除
- 黑客惡意攻擊,故意大量訪問某些讀取不存在數據的業務
解決緩存穿透
-
非法請求的限制
判斷請求參數是否含有非法值?請求字段是否存在?
-
緩存空值或默認值
當線上業務發現緩存穿透時,針對查詢的數據,在緩存中設置一個空值或默認值,后續請求可以從緩存中讀取到數據,而不會繼續查詢數據庫
-
使用布隆過濾器快速判斷數據是否存在,避免通過查詢數據庫來判斷數據是否存在。
寫入數據庫數據時,使用布隆過濾器做標記,當業務線程確認緩存失效后,可以通過查詢布隆過濾器判斷數據是否存在。(大量請求只會查詢布隆過濾器和redis,而不會查詢數據庫)
注:布隆過濾器的實現
設此時有3個哈希函數,位圖數組長度為8,數據庫寫入數據x:
將該數據x得到的三個哈希值 % 位圖數據長度得到三個數組下標,填入1。
當業務線程查詢數據是否存在于數據庫時,查詢 1、4、6下標的值是否為1,若有一個為0,則說明不存在
(存在哈希沖突,故若查詢布隆過濾器說數據存在于數據庫,此時數據不一定在數據庫;但是查詢到數據不存在時,數據一定不存在)
更新數據時,如何保證數據庫和緩存的一致性?
1. 先更新數據庫?先更新緩存?
在數據更新時,先更新數據庫還是先更新緩存,都會存在并發問題,當兩個請求并發更新同一條數據時,可能會出現緩存和數據庫中數據不一致的現象。
解決方案
- 更新緩存之前加分布式鎖,保證同一時間只運行一個請求更新緩存,但對寫入性能造成影響
- 更新完緩存后,給緩存加上較短的過期時間,即使不一致,但也會很快過期
2. Cache Aside策略
旁路緩存策略: 在更新數據時,不更新緩存,更新數據庫,刪除緩存, 當讀取數據發現緩存中無該數據時,再從數據庫中讀取數據,并且寫入緩存。
(刪除緩存,不更新緩存是懶加載思想的應用)
分為讀策略、寫策略
- 寫策略
- 更新數據庫中的數據
- 刪除緩存中的數據
- 讀策略
- 若讀取的數據命中緩存,則直接返回數據
- 若讀取的數據沒有命中緩存,則從數據庫中讀取數據,再將該數據寫入緩存,并且返回給用戶
例:請求A讀取數據,請求B更新數據
此時數據庫中為21,緩存中為20
該情況出現概率不高,因為緩存的寫入通常遠遠快于數據庫的寫入
① 先更新數據庫,再刪除緩存
故 先更新數據庫,再刪除緩存 可以保證“數據一致性”,并且對緩存加上過期時間,可以保證最終一致性
問題:
-
先更新數據庫,再刪除緩存會導致緩存命中率降低。
? 若對緩存命中率有要求,可以采用更新數據庫+更新緩存,解決方案見1.
-
這種方法保證數據一致性的前提是 更新數據庫和刪除緩存都能正常執行成功。
(刪除緩存失敗時,可能出現緩存中為舊數據,數據庫中為新數據)
保證更新數據庫、刪除緩存都執行成功
采用異步緩存,保證第二個操作執行成功
- 重試機制 => 引入消息隊列,將刪除緩存操作的數據加入消息隊列,由消費者操作數據
- 如果刪除緩存失敗,從消息隊列重新讀取需要刪除的數據,再次刪除緩存(若多次刪除失敗,需要向業務層發送報錯信息)
- 如果刪除緩存成功,把數據從消息隊列移除,避免重復操作
- 訂閱 MySQL binlog,再操作緩存
- 先更新數據庫,再刪除緩存,當更新數據庫成功,就會產生一條變更日志,記錄在binlog里。于是可以訂閱binlog日志,拿到具體要操作的數據,再執行緩存刪除。
- Canal模擬MySQL的主從復制的交互協議,把自己偽裝成從節點,向MySQL主節點發送dump請求,MySQL收到請求后,推送binlog給Canal,Canal解析Binlog字節流后,轉換為便于讀取的結構化數據,供下游程序訂閱使用
- 重試機制 => 引入消息隊列,將刪除緩存操作的數據加入消息隊列,由消費者操作數據
② 先刪除緩存,再更新數據庫
出現并發問題,造成緩存、數據庫數據不一致
解決方案
延遲雙刪
- 刪除緩存
- 更新數據庫
- 睡眠
- 再刪除緩存
請求A在睡眠時,B能夠完成讀取數據庫數據,并把缺失數據寫入緩存,A睡眠完后刪除緩存。
請求A的睡眠時間 > 請求B的從數據庫讀取數據+寫入緩存的時間
該方案盡可能保持一致性,建議采用先更新數據庫,再刪除緩存
互斥鎖vs分布式鎖
互斥鎖:單機情況下,內存中的一個互斥鎖就能控制一個程序的線程并發
分布式鎖:適用于分布式場景,集群架構,需要“全局鎖”實現控制多個程序/多個機器上的線程并發
小林coding圖解Redis — 七