在咱們平常的開發中,無不都是使用數據庫來進行數據的存儲,因爲通常的系統任務中一般不會存在高併發的狀況,因此這樣看起來並無什麼問題,但是一旦涉及大數據量的需求,好比一些商品搶購的情景,或者是主頁訪問量瞬間較大的時候,單一使用數據庫來保存數據的系統會由於面向磁盤,磁盤讀/寫速度比較慢的問題而存在嚴重的性能弊端,一瞬間成千上萬的請求到來,須要系統在極短的時間內完成成千上萬次的讀/寫操做,這個時候每每不是數據庫可以承受的,極其容易形成數據庫系統癱瘓,最終致使服務宕機的嚴重生產問題。java
爲了克服上述的問題,項目一般會引入NoSQL技術,這是一種基於內存的數據庫,而且提供必定的持久化功能。redis
redis技術就是NoSQL技術中的一種,可是引入redis又有可能出現緩存穿透,緩存擊穿,緩存雪崩等問題。本文就對這三種問題進行較深刻剖析。sql
一個必定不存在緩存及查詢不到的數據,因爲緩存是不命中時被動寫的,而且出於容錯考慮,若是從存儲層查不到數據則不寫入緩存,這將致使這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。數據庫
有不少種方法能夠有效地解決緩存穿透問題,最多見的則是採用布隆過濾器,將全部可能存在的數據哈希到一個足夠大的bitmap中,一個必定不存在的數據會被 這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。另外也有一個更爲簡單粗暴的方法(咱們採用的就是這種),若是一個查詢返回的數據爲空(不論是數據不存在,仍是系統故障),咱們仍然把這個空結果進行緩存,但它的過時時間會很短,最長不超過五分鐘。後端
粗暴方式僞代碼:緩存
//僞代碼 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //數據庫查詢不到,爲空 cacheValue = GetProductListFromDB(); if (cacheValue == null) { //若是發現爲空,設置個默認值,也緩存起來 cacheValue = string.Empty; } CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; } }
key可能會在某些時間點被超高併發地訪問,是一種很是「熱點」的數據。這個時候,須要考慮一個問題:緩存被「擊穿」的問題。安全
使用互斥鎖(mutex key)服務器
業界比較經常使用的作法,是使用mutex。簡單地來講,就是在緩存失效的時候(判斷拿出來的值爲空),不是當即去load db,而是先使用緩存工具的某些帶成功操做返回值的操做(好比Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操做返回成功時,再進行load db的操做並回設緩存;不然,就重試整個get緩存的方法。併發
SETNX,是「SET if Not eXists」的縮寫,也就是隻有不存在的時候才設置,能夠利用它來實現鎖的效果。分佈式
public String get(key) { String value = redis.get(key); if (value == null) { //表明緩存值過時 //設置3min的超時,防止del操做失敗的時候,下次緩存過時一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //表明設置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //這個時候表明同時候的其餘線程已經load db並回設到緩存了,這時候重試獲取緩存值便可 sleep(50); get(key); //重試 } } else { return value; } }
memcache代碼:
if (memcache.get(key) == null) { // 3 min timeout to avoid mutex holder crash if (memcache.add(key_mutex, 3 * 60 * 1000) == true) { value = db.get(key); memcache.set(key, value); memcache.delete(key_mutex); } else { sleep(50); retry(); } }
其它方案:待各位補充。
與緩存擊穿的區別在於這裏針對不少key緩存,前者則是某一個key。
緩存正常從Redis中獲取,示意圖以下:
緩存失效瞬間示意圖以下:
緩存失效時的雪崩效應對底層系統的衝擊很是可怕!大多數系統設計者考慮用加鎖或者隊列的方式保證來保證不會有大量的線程對數據庫一次性進行讀寫,從而避免失效時大量的併發請求落到底層存儲系統上。還有一個簡單方案就時講緩存失效時間分散開,好比咱們能夠在原有的失效時間基礎上增長一個隨機值,好比1-5分鐘隨機,這樣每個緩存的過時時間的重複率就會下降,就很難引起集體失效的事件。
加鎖排隊,僞代碼以下:
//僞代碼 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //這裏通常是sql查詢數據 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } }
加鎖排隊只是爲了減輕數據庫的壓力,並無提升系統吞吐量。假設在高併發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。一樣會致使用戶等待超時,這是個治標不治本的方法!
注意:加鎖排隊的解決方式分佈式環境的併發問題,有可能還要解決分佈式鎖的問題;線程還會被阻塞,用戶體驗不好!所以,在真正的高併發場景下不多使用!
隨機值僞代碼:
//僞代碼 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //緩存標記 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //獲取緩存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未過時,直接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //這裏通常是 sql查詢數據 cacheValue = GetProductListFromDB(); //日期設緩存時間的2倍,用於髒讀 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; } }
解釋說明:
關於緩存崩潰的解決方法,這裏提出了三種方案:使用鎖或隊列、設置過時標誌更新緩存、爲key設置不一樣的緩存失效時間,還有一種被稱爲「二級緩存」的解決方法。
針對業務系統,永遠都是具體狀況具體分析,沒有最好,只有最合適。
於緩存其它問題,緩存滿了和數據丟失等問題,大夥可自行學習。最後也提一下三個詞LRU、RDB、AOF,一般咱們採用LRU策略處理溢出,Redis的RDB和AOF持久化策略來保證必定狀況下的數據安全。
參考相關連接:
https://blog.csdn.net/zeb_per...
https://blog.csdn.net/fanrenx...
https://baijiahao.baidu.com/s...
https://blog.csdn.net/xlgen15...
視頻資源獲取,可直進百度雲羣:
https://pan.baidu.com/mbox/ho...
本文在米兜公衆號連接
https://mp.weixin.qq.com/s/ks...
歡迎關注米兜Java,一個注在共享、交流的Java學習平臺。