01前言
在我們日常的開發中,無不都是使用數據庫來進行數據的存儲,由于一般的系統任務中通常不會存在高并發的情況,所以這樣看起來并沒有什么問題,可是一旦涉及大數據量的需求,比如一些商品搶購的情景,或者是主頁訪問量瞬間較大的時候,單一使用數據庫來保存數據的系統會因為面向磁盤,磁盤讀/寫速度比較慢的問題而存在嚴重的性能弊端,一瞬間成千上萬的請求到來,需要系統在極短的時間內完成成千上萬次的讀/寫操作,這個時候往往不是數據庫能夠承受的,極其容易造成數據庫系統癱瘓,最終導致服務宕機的嚴重生產問題。
為了克服上述的問題,項目通常會引入NoSQL技術,這是一種基于內存的數據庫,并且提供一定的持久化功能。
redis技術就是NoSQL技術中的一種,但是引入redis又有可能出現緩存穿透,緩存擊穿,緩存雪崩等問題。本文就對這三種問題進行較深入剖析。
02初認識
- 緩存穿透:key對應的數據在數據源并不存在,每次針對此key的請求從緩存獲取不到,請求都會到數據源,從而可能壓垮數據源。比如用一個不存在的用戶id獲取用戶信息,不論緩存還是數據庫都沒有,若黑客利用此漏洞進行攻擊可能壓垮數據庫。
- 緩存擊穿:key對應的數據存在,但在redis中過期,此時若有大量并發請求過來,這些請求發現緩存過期一般都會從后端DB加載數據并回設到緩存,這個時候大并發的請求可能會瞬間把后端DB壓垮。
- 緩存雪崩:當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(比如DB)帶來很大壓力。
03緩存穿透解決方案
一個一定不存在緩存及查詢不到的數據,由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。
有很多種方法可以有效地解決緩存穿透問題,最常見的則是采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的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; } }
04緩存擊穿解決方案
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(); } }
其它方案:待各位補充。
05緩存雪崩解決方案
與緩存擊穿的區別在于這里針對很多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的緩存;
- 緩存數據:它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鐘,數據緩存設置為60分鐘。這樣,當緩存標記key過期后,實際緩存還能把舊數據返回給調用端,直到另外的線程在后臺更新完成后,才會返回新緩存。
關于緩存崩潰的解決方法,這里提出了三種方案:使用鎖或隊列、設置過期標志更新緩存、為key設置不同的緩存失效時間,還有一種被稱為“二級緩存”的解決方法。
06小結
針對業務系統,永遠都是具體情況具體分析,沒有最好,只有最合適。
于緩存其它問題,緩存滿了和數據丟失等問題,大伙可自行學習。最后也提一下三個詞LRU、RDB、AOF,通常我們采用LRU策略處理溢出,Redis的RDB和AOF持久化策略來保證一定情況下的數據安全。
參考相關鏈接:
https://blog.csdn.net/zeb_perfect/article/details/54135506
https://blog.csdn.net/fanrenxiang/article/details/80542580
https://baijiahao.baidu.com/s?id=1619572269435584821&wfr=spider&for=pc
https://blog.csdn.net/xlgen157387/article/details/79530877
END!
請留下你指尖的溫度
讓太陽擁抱你
記得這是一個有溫度的頭條號
print_r('點個好看吧!');
var_dump('點個好看吧!');
NSLog(@"點個好看吧!");
System.out.println("點個好看吧!");
console.log("點個好看吧!");
print("點個好看吧!");
printf("點個好看吧!n");
cout << "點個好看吧!" << endl;
Console.WriteLine("點個好看吧!");
fmt.Println("點個好看吧!");
Response.Write("點個好看吧!");
alert("點個好看吧!")
echo"點個好看吧!"