面試題:你平時(shí)是如何解決緩存穿透蜀肘,緩存擊穿,緩存雪崩的稽屏?

# 前言

設(shè)計(jì)一個(gè)緩存系統(tǒng)扮宠,不得不要考慮的問(wèn)題就是:緩存穿透、緩存擊穿與失效時(shí)的雪崩效應(yīng)狐榔。

# 緩存穿透

緩存穿透是指查詢(xún)一個(gè)一定不存在的數(shù)據(jù)坛增,由于緩存是不命中時(shí)被動(dòng)寫(xiě)的获雕,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫(xiě)入緩存轿偎,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢(xún)典鸡,失去了緩存的意義。

在流量大時(shí)坏晦,可能DB就掛掉了萝玷,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞昆婿。

# 解決方案

有很多種方法可以有效地解決緩存穿透問(wèn)題球碉,最常見(jiàn)的則是采用布隆過(guò)濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中仓蛆,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmap攔截掉睁冬,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢(xún)壓力。

另外也有一個(gè)更為簡(jiǎn)單粗暴的方法(我們采用的就是這種)看疙,如果一個(gè)查詢(xún)返回的數(shù)據(jù)為空(不管是數(shù) 據(jù)不存在豆拨,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存能庆,但它的過(guò)期時(shí)間會(huì)很短施禾,最長(zhǎng)不超過(guò)五分鐘。

# 緩存雪崩

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

# 解決方案

緩存失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非撑世可怕。大多數(shù)系統(tǒng)設(shè)計(jì)者考慮用加鎖或者隊(duì)列的方式保證緩存的單線(xiàn) 程(進(jìn)程)寫(xiě)顾腊,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上粤铭。

這里分享一個(gè)簡(jiǎn)單方案就是講緩存失效時(shí)間分散開(kāi),比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值杂靶,比如1-5分鐘隨機(jī)承耿,這樣每一個(gè)緩存的過(guò)期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件伪煤。

# 緩存擊穿

對(duì)于一些設(shè)置了過(guò)期時(shí)間的key,如果這些key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪(fǎng)問(wèn)凛辣,是一種非潮Ъ龋“熱點(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候扁誓,需要考慮一個(gè)問(wèn)題:緩存被“擊穿”的問(wèn)題防泵,這個(gè)和緩存雪崩的區(qū)別在于這里針對(duì)某一key緩存蚀之,前者則是很多key。

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

# 解決方案

1.使用互斥鎖(mutex key)

業(yè)界比較常用的做法失受,是使用mutex。簡(jiǎn)單地來(lái)說(shuō)咏瑟,就是在緩存失效的時(shí)候(判斷拿出來(lái)的值為空)拂到,不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個(gè)mutex key码泞,當(dāng)操作返回成功時(shí)兄旬,再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則余寥,就重試整個(gè)get緩存的方法领铐。

SETNX,是「SET if Not eXists」的縮寫(xiě)宋舷,也就是只有不存在的時(shí)候才設(shè)置绪撵,可以利用它來(lái)實(shí)現(xiàn)鎖的效果。在redis2.6.1之前版本未實(shí)現(xiàn)setnx的過(guò)期時(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 {?

? ? ? ? //其他線(xiàn)程休息50毫秒后重試?

? ? ? ? Thread.sleep(50);?

? ? ? ? get(key);?

? ? }?

? }?

}

新版本代碼:

public String get(key) {

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

? ? ? if (value == null) { //代表緩存值過(guò)期

? ? ? ? ? //設(shè)置3min的超時(shí)莲兢,防止del操作失敗的時(shí)候,下次緩存過(guò)期一直不能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 {? //這個(gè)時(shí)候代表同時(shí)候的其他線(xiàn)程已經(jīng)load db并回設(shè)到緩存了续膳,這時(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();?

? ? }?

}

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

在value內(nèi)部設(shè)置1個(gè)超時(shí)值(timeout1), timeout1比實(shí)際的memcache timeout(timeout2)小改艇。

當(dāng)從cache讀取到timeout1發(fā)現(xiàn)它已經(jīng)過(guò)期時(shí)候,馬上延長(zhǎng)timeout1并重新設(shè)置到cache坟岔。然后再?gòu)臄?shù)據(jù)庫(kù)加載數(shù)據(jù)并設(shè)置到cache中谒兄。

偽代碼如下:

v = memcache.get(key);?

if (v == null) {?

? ? 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();?

? ? }?

} else {?

? ? if (v.timeout <= now()) {?

? ? ? ? if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {?

? ? ? ? ? ? // extend the timeout for other threads?

? ? ? ? ? ? v.timeout += 3 * 60 * 1000;?

? ? ? ? ? ? memcache.set(key, v, KEY_TIMEOUT * 2);

? ? ? ? ? ? // load the latest value from db?

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

? ? ? ? ? ? v.timeout = KEY_TIMEOUT;?

? ? ? ? ? ? memcache.set(key, value, KEY_TIMEOUT * 2);?

? ? ? ? ? ? memcache.delete(key_mutex);?

? ? ? ? } else {?

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

? ? ? ? ? ? retry();?

? ? ? ? }?

? ? }?

}

3. "永遠(yuǎn)不過(guò)期"

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

(1) 從redis上看,確實(shí)沒(méi)有設(shè)置過(guò)期時(shí)間社付,這就保證了承疲,不會(huì)出現(xiàn)熱點(diǎn)key過(guò)期問(wèn)題,也就是“物理”不過(guò)期鸥咖。

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

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

String get(final String key) {?

? ? ? ? V v = redis.get(key);?

? ? ? ? String value = v.getValue();?

? ? ? ? long timeout = v.getTimeout();?

? ? ? ? if (v.timeout <= System.currentTimeMillis()) {?

? ? ? ? ? ? // 異步更新后臺(tái)異常執(zhí)行?

? ? ? ? ? ? threadPool.execute(new Runnable() {?

? ? ? ? ? ? ? ? public void run() {?

? ? ? ? ? ? ? ? ? ? String keyMutex = "mutex:" + key;?

? ? ? ? ? ? ? ? ? ? if (redis.setnx(keyMutex, "1")) {?

? ? ? ? ? ? ? ? ? ? ? ? // 3 min timeout to avoid mutex holder crash?

? ? ? ? ? ? ? ? ? ? ? ? redis.expire(keyMutex, 3 * 60);?

? ? ? ? ? ? ? ? ? ? ? ? String dbValue = db.get(key);?

? ? ? ? ? ? ? ? ? ? ? ? redis.set(key, dbValue);?

? ? ? ? ? ? ? ? ? ? ? ? redis.delete(keyMutex);?

? ? ? ? ? ? ? ? ? ? }?

? ? ? ? ? ? ? ? }?

? ? ? ? ? ? });?

? ? ? ? }?

? ? ? ? return value;?

}

4. 資源保護(hù)

采用netflix的hystrix,可以做資源的隔離保護(hù)主線(xiàn)程池济似,如果把這個(gè)應(yīng)用到緩存的構(gòu)建也未嘗不可矫废。

四種解決方案:沒(méi)有最佳只有最合適

解決方案

優(yōu)點(diǎn)

缺點(diǎn)

簡(jiǎn)單分布式鎖(Tim yang)

1. 思路簡(jiǎn)單

2. 保證一致性

1. 代碼復(fù)雜度增大

2. 存在死鎖的風(fēng)險(xiǎn)

3. 存在線(xiàn)程池阻塞的風(fēng)險(xiǎn)

加另外一個(gè)過(guò)期時(shí)間(Tim yang)

1. 保證一致性

同上

不過(guò)期(本文)

1. 異步構(gòu)建緩存,不會(huì)阻塞線(xiàn)程池

1. 不保證一致性碱屁。

2. 代碼復(fù)雜度增大(每個(gè)value都要維護(hù)一個(gè)timekey)磷脯。

3. 占用一定的內(nèi)存空間(每個(gè)value都要維護(hù)一個(gè)timekey)。

資源隔離組件hystrix(本文)

1. hystrix技術(shù)成熟娩脾,有效保證后端赵誓。

2. hystrix監(jiān)控強(qiáng)大。

1. 部分訪(fǎng)問(wèn)存在降級(jí)策略柿赊。

# 總結(jié)

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末豺谈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贡这,更是在濱河造成了極大的恐慌茬末,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盖矫,死亡現(xiàn)場(chǎng)離奇詭異丽惭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)辈双,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)责掏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人湃望,你說(shuō)我怎么就攤上這事换衬【忠澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵冗疮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我檩帐,道長(zhǎng)术幔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任湃密,我火速辦了婚禮诅挑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘泛源。我一直安慰自己拔妥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布达箍。 她就那樣靜靜地躺著没龙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缎玫。 梳的紋絲不亂的頭發(fā)上硬纤,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音赃磨,去河邊找鬼筝家。 笑死,一個(gè)胖子當(dāng)著我的面吹牛邻辉,可吹牛的內(nèi)容都是我干的溪王。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼值骇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼莹菱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起雷客,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芒珠,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后搅裙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體皱卓,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年部逮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了娜汁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兄朋,死狀恐怖掐禁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤傅事,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布缕允,位于F島的核電站,受9級(jí)特大地震影響蹭越,放射性物質(zhì)發(fā)生泄漏障本。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一响鹃、第九天 我趴在偏房一處隱蔽的房頂上張望驾霜。 院中可真熱鬧,春花似錦买置、人聲如沸粪糙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蓉冈。三九已至,卻和暖如春倦卖,著一層夾襖步出監(jiān)牢的瞬間洒擦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工怕膛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熟嫩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓褐捻,卻偏偏與公主長(zhǎng)得像掸茅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柠逞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容