缓存穿透、缓存击穿和缓存雪崩
我們使用緩存的主要目是提升查詢速度和保護(hù)數(shù)據(jù)庫(kù)等稀缺資源不被占滿。
而緩存最常見(jiàn)的問(wèn)題是緩存穿透、擊穿和雪崩,在高并發(fā)下這三種情況都會(huì)有大量請(qǐng)求落到數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)資源占滿,引起數(shù)據(jù)庫(kù)故障。今天我主要分享一下layering-cache緩存框架在這個(gè)三個(gè)問(wèn)題上的實(shí)踐方案。
?
?
概念
?
緩存穿透
在高并發(fā)下,查詢一個(gè)不存在的值時(shí),緩存不會(huì)被命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面查詢一個(gè)不存在的活動(dòng)。
?
緩存擊穿
在高并發(fā)下,對(duì)一個(gè)特定的值進(jìn)行查詢,但是這個(gè)時(shí)候緩存正好過(guò)期了,緩存沒(méi)有命中,導(dǎo)致大量請(qǐng)求直接落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面查詢活動(dòng)信息,但是在活動(dòng)進(jìn)行過(guò)程中活動(dòng)緩存突然過(guò)期了。
?
緩存雪崩
在高并發(fā)下,大量的緩存key在同一時(shí)間失效,導(dǎo)致大量的請(qǐng)求落到數(shù)據(jù)庫(kù)上,如活動(dòng)系統(tǒng)里面同時(shí)進(jìn)行著非常多的活動(dòng),但是在某個(gè)時(shí)間點(diǎn)所有的活動(dòng)緩存全部過(guò)期。
?
常見(jiàn)解決方案
-
直接緩存NULL值
-
限流
-
緩存預(yù)熱
-
分級(jí)緩存
-
緩存永遠(yuǎn)不過(guò)期
?
?
layering-cache實(shí)踐
在layering-cache里面結(jié)合了緩存NULL值,緩存預(yù)熱,限流、分級(jí)緩存和間接的實(shí)現(xiàn)"永不過(guò)期"等幾種方案來(lái)應(yīng)對(duì)緩存穿透、擊穿和雪崩問(wèn)題。
?
直接緩存NULL值
應(yīng)對(duì)緩存穿透最有效的方法是直接緩存NULL值,但是緩存NULL的時(shí)間不能太長(zhǎng),否則NULL數(shù)據(jù)長(zhǎng)時(shí)間得不到更新,也不能太短,否則達(dá)不到防止緩存擊穿的效果。
?
我在layering-cache對(duì)NULL值進(jìn)行了特殊處理,一級(jí)緩存不允許存NULL值,二級(jí)緩存可以配置緩存是否允許存NULL值,如果配置可以允許存NULL值,框架還支持配置緩存非空值和NULL值之間的過(guò)期時(shí)間倍率,這使得我們能精準(zhǔn)的控制每一個(gè)緩存的NULL值過(guò)期時(shí)間,控制粒度非常細(xì)。當(dāng)NULL緩存過(guò)期我還可以使用限流,緩存預(yù)熱等手段來(lái)防止穿透。
?
示例:
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
?
在這個(gè)例子里面isAllowNullValue = true表示允許緩存NULL值,magnification = 10表示NULL值和非NULL值之間的時(shí)間倍率是10,也就是說(shuō)當(dāng)緩存值為NULL值,二級(jí)緩存的有效時(shí)間將是1個(gè)小時(shí)。
?
限流
應(yīng)對(duì)緩存穿透的常用方法之一是限流,常見(jiàn)的限流算法有滑動(dòng)窗口,令牌桶算法和漏桶算法,或者直接使用隊(duì)列、加鎖等,在layering-cache里面我主要使用分布式鎖來(lái)做限流。
?
layering-cache數(shù)據(jù)讀取流程:?
數(shù)據(jù)讀取流程.jpg
?
下面是讀取數(shù)據(jù)的核心代碼:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
?
當(dāng)需要加載緩存的時(shí)候,需要獲取到鎖才有權(quán)限到后臺(tái)去加載緩存數(shù)據(jù),否則就會(huì)等待(同一個(gè)線程循環(huán)20次查詢緩存,每次等待20毫秒,如果還是沒(méi)有數(shù)據(jù)直接去執(zhí)行被緩存的方法,這個(gè)主要是為了防止獲取到鎖并且去加載緩存的線程出問(wèn)題,沒(méi)有返回而導(dǎo)致死鎖)。當(dāng)獲取到鎖的線程執(zhí)行完成會(huì)將獲取到的數(shù)據(jù)放到緩存中,并且喚醒所有等待線程。
?
這里需要注意一下讓線程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的時(shí)候,我發(fā)現(xiàn)當(dāng)并發(fā)達(dá)到300左右,緩存一旦過(guò)期就會(huì)引起死鎖,原因是使用的是sleep方法來(lái)讓沒(méi)有獲取到鎖的線程等待,當(dāng)?shù)却木€程很多的時(shí)候會(huì)產(chǎn)生大量上下文切換,導(dǎo)致獲取到鎖的線程一直獲取不到cpu的執(zhí)行權(quán),導(dǎo)致死鎖。在layering-cache里面,我們使用的是LockSupport.parkNanos方法,它會(huì)釋放cpu資源, 因?yàn)槲覀兪褂玫氖莚edis分布式鎖,所以也不能使用wait-notify機(jī)制。
?
緩存預(yù)熱
有效應(yīng)對(duì)緩存的擊穿和雪崩的方式之一是緩存預(yù)加載。
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
?
在 layering-cache里面二級(jí)緩存會(huì)配置兩個(gè)時(shí)間,expireTime是緩存的過(guò)期時(shí)間,preloadTime 是緩存的刷新時(shí)間(預(yù)加載時(shí)間)。每次二級(jí)緩存被命中都會(huì)去檢查緩存的過(guò)去時(shí)間是否小于刷新時(shí)間,如果小于就會(huì)開(kāi)啟一個(gè)異步線程預(yù)先去更新緩存,并將新的值放到緩存中,有效的保證了熱點(diǎn)數(shù)據(jù)**"永不過(guò)期"**。這里預(yù)先更新緩存也是需要加鎖的,并不是所有的線程都會(huì)落到庫(kù)上刷新緩存,如果沒(méi)有獲取到鎖就直接結(jié)束當(dāng)前線程。
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
?
在緩存總量和并發(fā)量都很大的時(shí)候,這個(gè)時(shí)候緩存如果同時(shí)失效,緩存預(yù)熱將是一個(gè)非常慢長(zhǎng)的過(guò)程,就比如說(shuō)服務(wù)重啟或新上線一個(gè)新的緩存。這個(gè)時(shí)候我們可以采用切流的方式,讓緩存慢慢預(yù)熱,如開(kāi)始切10%流量,觀察沒(méi)有異常后,再切30%流量,觀察沒(méi)有異常后,再切60%流量,然后全量。這種方式雖然有點(diǎn)繁瑣,但是一旦遇到異常我們可以快速的切回流量,讓風(fēng)險(xiǎn)可控。
?
總結(jié)
總體來(lái)說(shuō)layering-cache在緩存穿透、擊穿和雪崩上是以預(yù)防為主,補(bǔ)救為輔。而在應(yīng)對(duì)緩存的這些問(wèn)題上其實(shí)也沒(méi)有一個(gè)完全完美的方案,只有最適合自己業(yè)務(wù)系統(tǒng)的方案。目前如果直接使用layering-cache緩存框架已經(jīng)基本能應(yīng)對(duì)大部分的緩存問(wèn)題了。
?
源碼
https://github.com/xiaolyuh/layering-cache
?
layering-cache
為監(jiān)控而生的多級(jí)緩存框架 layering-cache這是我開(kāi)源的一個(gè)多級(jí)緩存框架的實(shí)現(xiàn),如果有興趣可以看一下
GitHub地址:https://github.com/xiaolyuh/layering-cache
?
作者:xiaolyuh
https://my.oschina.net/u/3748347/blog/2995017
總結(jié)
以上是生活随笔為你收集整理的缓存穿透、缓存击穿和缓存雪崩的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: layering-cache
- 下一篇: 怎么可以让额头长出头发