如何防止緩存擊穿?
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??zheski
來(lái)源 |? urlify.cn/a6zeIj
66套java從入門到精通實(shí)戰(zhàn)課程分享
緩存擊穿
在使用緩存時(shí),我們往往是先根據(jù)key從緩存中取數(shù)據(jù),如果拿不到就去數(shù)據(jù)源加載數(shù)據(jù),寫入緩存。但是在某些高并發(fā)的情況下,可能會(huì)出現(xiàn)緩存擊穿的問(wèn)題,比如一個(gè)存在的key,在緩存過(guò)期的一刻,同時(shí)有大量的請(qǐng)求,這些請(qǐng)求都會(huì)擊穿到DB,造成瞬時(shí)DB請(qǐng)求量大、壓力驟增。
一般解決方案
首先我們想到的解決方案就是加鎖,一種辦法是:拿到鎖的請(qǐng)求,去加載數(shù)據(jù),沒(méi)有拿到鎖的請(qǐng)求,就先等待。這種方法雖然避免了并發(fā)加載數(shù)據(jù),但實(shí)際上是將并發(fā)的操作串行化,會(huì)增加系統(tǒng)延時(shí)。
singleflight
singleflight是groupcache這個(gè)項(xiàng)目的一部分,groupcache是memcache作者使用golang編寫的分布式緩存。singleflight能夠使多個(gè)并發(fā)請(qǐng)求的回源操作中,只有第一個(gè)請(qǐng)求會(huì)進(jìn)行回源操作,其他的請(qǐng)求會(huì)阻塞等待第一個(gè)請(qǐng)求完成操作,直接取其結(jié)果,這樣可以保證同一時(shí)刻只有一個(gè)請(qǐng)求在進(jìn)行回源操作,從而達(dá)到防止緩存擊穿的效果。下面是參考groupcache源碼,使用Java實(shí)現(xiàn)的singleflight代碼:
//代表正在進(jìn)行中,或已經(jīng)結(jié)束的請(qǐng)求
public?class?Call?{
????private?byte[]?val;
????private?CountDownLatch?cld;
????public?byte[]?getVal()?{
????????return?val;
????}
????public?void?setVal(byte[]?val)?{
????????this.val?=?val;
????}
????public?void?await()?{
????????try?{
????????????this.cld.await();
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????}
????public?void?lock()?{
????????this.cld?=?new?CountDownLatch(1);
????}
????public?void?done()?{
????????this.cld.countDown();
????}
}
//singleflight?的主類,管理不同?key?的請(qǐng)求(call)
public?class?CallManage?{
????private?final?Lock?lock?=?new?ReentrantLock();
????private?Map?callMap;
????public?byte[]?run(String?key,?Supplier?func)?{
????????this.lock.lock();
????????if?(this.callMap?==?null)?{
????????????this.callMap?=?new?HashMap<>();
????????}
????????Call?call?=?this.callMap.get(key);
????????if?(call?!=?null)?{
????????????this.lock.unlock();
????????????call.await();
????????????return?call.getVal();
????????}
????????call?=?new?Call();
????????call.lock();
????????this.callMap.put(key,?call);
????????this.lock.unlock();
????????call.setVal(func.get());
????????call.done();
????????this.lock.lock();
????????this.callMap.remove(key);
????????this.lock.unlock();
????????return?call.getVal();
????}
}
我們使用CountDownLatch來(lái)實(shí)現(xiàn)多個(gè)線程等待一個(gè)線程完成操作,CountDownLatch包含一個(gè)計(jì)數(shù)器,初始化時(shí)賦值,countDown()可使計(jì)數(shù)器減一,當(dāng)count為0時(shí)喚醒所有等待的線程,await()可使線程阻塞。我們同樣用CountDownLatch來(lái)模擬一個(gè)10次并發(fā),測(cè)試代碼如下:
public?static?void?main(String[]?args)?{
????CallManage?callManage?=?new?CallManage();
????int?count?=?10;
????CountDownLatch?cld?=?new?CountDownLatch(count);
????for?(int?i?=?0;?i?????????new?Thread(()?->?{
????????????try?{
????????????????cld.await();
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????????byte[]?value?=?callManage.run("key",?()?->?{
????????????????System.out.println("func");
????????????????return?ByteArrayUtil.oToB("bar");
????????????});
????????????System.out.println(ByteArrayUtil.bToO(value).toString());
????????}).start();
????????cld.countDown();
????}
}
測(cè)試結(jié)果如下:
func
bar
bar
bar
bar
bar
bar
bar
bar
bar
bar
可以看到回源操作只被執(zhí)行了一次,其他9次直接取到了第一次操作的結(jié)果。
總結(jié)
可以看到singleflight可以有效解決高并發(fā)情況下的緩存擊穿問(wèn)題,singleflight這種控制機(jī)制不僅可以用在緩存擊穿的問(wèn)題上,理論上可以解決各種分層結(jié)構(gòu)的高并發(fā)性能問(wèn)題。
粉絲福利:108本java從入門到大神精選電子書領(lǐng)取
???
?長(zhǎng)按上方鋒哥微信二維碼?2 秒 備注「1234」即可獲取資料以及 可以進(jìn)入java1234官方微信群
感謝點(diǎn)贊支持下哈?
