緩存穿透,擊穿,雪崩解決方案分析

設(shè)計(jì)一個(gè)緩存系統(tǒng)需要考慮:緩存穿透,緩存擊穿、失效時(shí)得雪崩效應(yīng)。

緩存穿透:緩存穿透是指查詢一個(gè)一定不存在數(shù)據(jù),由于緩存是不命中時(shí)被動(dòng)寫的,并且處于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在得數(shù)據(jù)每次請(qǐng)求都要從存儲(chǔ)層去查詢,失去了緩存得意義。在流量大時(shí),可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。

解決方案:

布隆過濾器:將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一個(gè)不存在的數(shù)據(jù)會(huì)被bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的壓力。

如果一個(gè)查詢返回的數(shù)據(jù)為空(不管系統(tǒng)故障還是數(shù)據(jù)不存在),我們?nèi)园堰@個(gè)空結(jié)果緩存,但它的過期時(shí)間會(huì)很短,最長(zhǎng)不超過5分鐘。

緩存雪崩:指在我們?cè)O(shè)置了緩存時(shí)采用了相同的過期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,請(qǐng)求全部轉(zhuǎn)發(fā)到DB,DB瞬時(shí)壓力過重雪崩。

解決方案:

緩存失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非常可怕。大多數(shù)系統(tǒng)設(shè)計(jì)者考慮加鎖或者隊(duì)列的方式保證緩存單線程(進(jìn)程)寫從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上 。分享一個(gè)簡(jiǎn)單的方案將緩存失效時(shí)間分散開,比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過期時(shí)間重復(fù)率降低,就不容易引發(fā)集體失效事件。

緩存擊穿:對(duì)于一些設(shè)置了過期時(shí)間的key,如果這些key 可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)的訪問,是一種非?!盁狳c(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候需要考慮一個(gè)問題:緩存被擊穿的問題,這個(gè)和緩存雪崩的區(qū)別在于,這里是針對(duì)某一key緩存,前者則是很多key.

緩存在某個(gè)時(shí)間點(diǎn)過期的時(shí)候,恰好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)key有大量的并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把DB壓垮。

解決方案:

1、使用互斥鎖(mutex key)

簡(jiǎn)單來說,就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db ,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個(gè)mutex key,當(dāng)操作返回成功時(shí),再進(jìn)行 load db 的操作并回設(shè)緩存;否則,就重試整個(gè)get緩存方法。

SETNX,是 set if not exists 的縮寫,也就是不存在的時(shí)候才設(shè)置,可以利用它來實(shí)現(xiàn)鎖的效果。在 redis2.6.1之前的版本還未實(shí)現(xiàn)setnx 的過期時(shí)間,所以這里給出兩種版本代碼參考:

//2.6.1.前單機(jī)版本鎖

String get(String key){

??? String? value=redis.get(key);

if (value==null){

if(redis.setnx(key_mutex,1)){

//3 min timeout to avoid mutex holder crash

redis.expire(key_mutex,3*60);

value = db.get(key);

redis.set(key,value);

redis.delete(key_mutex);

}else{

//其他線程休息50毫秒重試

Thread.sleep(50);

get(key);

}

}

}

//新版本代碼

public String get(key){

???? String value=redis.get(key);

???? if (value==null){

???????? if (redis.setnx(key_mutex,1,3*60)==1){

?????????? value=db.get(key);

??????????? redis.set(key,value,expire_secx);

??????????? redis.del(key_mutex);

???????? }else{

??????????? sleep(50);

???????????? get(key);

????? }else{

???????? return value;

????? }

}

2,"提前“使用互斥鎖(mutex key)

在value內(nèi)部設(shè)置一個(gè)超時(shí)值(timeout1),timeout1比實(shí)際的memcache timeout(timeout2)小。當(dāng)從cache讀取到timeout1發(fā)現(xiàn)已經(jīng)過期時(shí),馬上延長(zhǎng) timeout1并重新設(shè)置到cache.然后再?gòu)臄?shù)據(jù)庫(kù)中加載數(shù)據(jù)并加載到cache中。

偽代碼如下:


3、“永遠(yuǎn)不過期”

這里的“永遠(yuǎn)不過期”包含兩層含義:

(1)從redis上看,確實(shí)沒有設(shè)置過期時(shí)間,這就保證了,不會(huì)出現(xiàn)熱點(diǎn)key過期問題,也就是“物理”的不過期

(2)從功能上看如果不過期,那不就成靜態(tài)的了么?所以我們把過期時(shí)間存在key對(duì)應(yīng)的value里,如果發(fā)現(xiàn)要過期了,通過一個(gè)后臺(tái)的異步線程進(jìn)行緩存構(gòu)建,也就是“邏輯”過期

從實(shí)戰(zhàn)看,這種方法對(duì)于性能非常友好,唯一不足的就是構(gòu)建緩存的時(shí)候,其余線程(非構(gòu)建緩存的線程)可能訪問的時(shí)老數(shù)據(jù),但是對(duì)于一般的互聯(lián)網(wǎng)功能來說這個(gè)還是可以忍受。


4、資源保護(hù)

采用netflix的hystrix,可以做資源的隔離保護(hù)主線程池,如果把這個(gè)應(yīng)用到緩存的構(gòu)建也是可以的。

四種解決方案沒有最佳只有最合適


總結(jié):

針對(duì)業(yè)務(wù)系統(tǒng),永遠(yuǎn)都是具體情況具體分析沒有最好只有最合適。

最后,對(duì)于緩存系統(tǒng)常見的緩存滿了和數(shù)據(jù)丟失問題,需要根據(jù)具體業(yè)務(wù)分析,通常我們采用LRU策咯處理溢出,Redis的RDB和AOF持久化策咯來保證一定情況的數(shù)據(jù)安全。

標(biāo)注:內(nèi)容來源于網(wǎng)絡(luò)公眾號(hào)等,非本人原創(chuàng) ~ ~。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容