您好,登錄后才能下訂單哦!
###一、前言
在我們?nèi)粘5拈_發(fā)中,無不都是使用數(shù)據(jù)庫來進(jìn)行數(shù)據(jù)的存儲,由于一般的系統(tǒng)任務(wù)中通常不會存在高并發(fā)的情況,所以這樣看起來并沒有什么問題,可是一旦涉及大數(shù)據(jù)量的需求,比如一些商品搶購的情景,或者是主頁訪問量瞬間較大的時候,單一使用數(shù)據(jù)庫來保存數(shù)據(jù)的系統(tǒng)會因?yàn)槊嫦虼疟P,磁盤讀/寫速度比較慢的問題而存在嚴(yán)重的性能弊端,一瞬間成千上萬的請求到來,需要系統(tǒng)在極短的時間內(nèi)完成成千上萬次的讀/寫操作,這個時候往往不是數(shù)據(jù)庫能夠承受的,極其容易造成數(shù)據(jù)庫系統(tǒng)癱瘓,最終導(dǎo)致服務(wù)宕機(jī)的嚴(yán)重生產(chǎn)問題。
為了克服上述的問題,項(xiàng)目通常會引入NoSQL技術(shù),這是一種基于內(nèi)存的數(shù)據(jù)庫,并且提供一定的持久化功能。
redis技術(shù)就是NoSQL技術(shù)中的一種,但是引入redis又有可能出現(xiàn)緩存穿透,緩存擊穿,緩存雪崩等問題。本文就對這三種問題進(jìn)行較深入剖析。
###二、初認(rèn)識
###三、緩存穿透解決方案
一個一定不存在緩存及查詢不到的數(shù)據(jù),由于緩存是不命中時被動寫的,并且出于容錯考慮,如果從存儲層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個不存在的數(shù)據(jù)每次請求都要到存儲層去查詢,失去了緩存的意義。
有很多種方法可以有效地解決緩存穿透問題,最常見的則是采用布隆過濾器,將所有可能存在的數(shù)據(jù)哈希到一個足夠大的bitmap中,一個一定不存在的數(shù)據(jù)會被 這個bitmap攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓力。另外也有一個更為簡單粗暴的方法(我們采用的就是這種),如果一個查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個空結(jié)果進(jìn)行緩存,但它的過期時間會很短,最長不超過五分鐘。
粗暴方式偽代碼:
//偽代碼
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 {
//數(shù)據(jù)庫查詢不到,為空
cacheValue = GetProductListFromDB();
if (cacheValue == null) {
//如果發(fā)現(xiàn)為空,設(shè)置個默認(rèn)值,也緩存起來
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
###四、緩存擊穿解決方案
key可能會在某些時間點(diǎn)被超高并發(fā)地訪問,是一種非?!盁狳c(diǎn)”的數(shù)據(jù)。這個時候,需要考慮一個問題:緩存被“擊穿”的問題。
使用互斥鎖(mutex key)
業(yè)界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個mutex key,當(dāng)操作返回成功時,再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個get緩存的方法。
SETNX,是「SET if Not eXists」的縮寫,也就是只有不存在的時候才設(shè)置,可以利用它來實(shí)現(xiàn)鎖的效果。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表緩存值過期
//設(shè)置3min的超時,防止del操作失敗的時候,下次緩存過期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表設(shè)置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //這個時候代表同時候的其他線程已經(jīng)load db并回設(shè)到緩存了,這時候重試獲取緩存值即可
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();
}
}
其它方案:待各位補(bǔ)充。
##五、緩存雪崩解決方案
與緩存擊穿的區(qū)別在于這里針對很多key緩存,前者則是某一個key。
緩存正常從Redis中獲取,示意圖如下:
緩存失效瞬間示意圖如下:
緩存失效時的雪崩效應(yīng)對底層系統(tǒng)的沖擊非??膳拢〈蠖鄶?shù)系統(tǒng)設(shè)計者考慮用加鎖或者隊(duì)列的方式保證來保證不會有大量的線程對數(shù)據(jù)庫一次性進(jìn)行讀寫,從而避免失效時大量的并發(fā)請求落到底層存儲系統(tǒng)上。還有一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎(chǔ)上增加一個隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個緩存的過期時間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。
加鎖排隊(duì),偽代碼如下:
//偽代碼
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查詢數(shù)據(jù)
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
加鎖排隊(duì)只是為了減輕數(shù)據(jù)庫的壓力,并沒有提高系統(tǒng)吞吐量。假設(shè)在高并發(fā)下,緩存重建期間key是鎖著的,這是過來1000個請求999個都在阻塞的。同樣會導(dǎo)致用戶等待超時,這是個治標(biāo)不治本的方法!
注意:加鎖排隊(duì)的解決方式分布式環(huán)境的并發(fā)問題,有可能還要解決分布式鎖的問題;線程還會被阻塞,用戶體驗(yàn)很差!因此,在真正的高并發(fā)場景下很少使用!
隨機(jī)值偽代碼:
//偽代碼
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
//緩存標(biāo)記
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查詢數(shù)據(jù)
cacheValue = GetProductListFromDB();
//日期設(shè)緩存時間的2倍,用于臟讀
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
解釋說明:
關(guān)于緩存崩潰的解決方法,這里提出了三種方案:使用鎖或隊(duì)列、設(shè)置過期標(biāo)志更新緩存、為key設(shè)置不同的緩存失效時間,還有一種被稱為“二級緩存”的解決方法。
##六、小結(jié)
針對業(yè)務(wù)系統(tǒng),永遠(yuǎn)都是具體情況具體分析,沒有最好,只有最合適。
于緩存其它問題,緩存滿了和數(shù)據(jù)丟失等問題,大伙可自行學(xué)習(xí)。最后也提一下三個詞LRU、RDB、AOF,通常我們采用LRU策略處理溢出,Redis的RDB和AOF持久化策略來保證一定情況下的數(shù)據(jù)安全。
參考相關(guān)鏈接:
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
視頻資源獲取,可直進(jìn)百度云群:
https://pan.baidu.com/mbox/homepage?short=btNBJoN
本文在米兜公眾號鏈接
https://mp.weixin.qq.com/s/ksVC1049wZgPIOy2gGziNA
歡迎關(guān)注米兜Java,一個注在共享、交流的Java學(xué)習(xí)平臺。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。