一、緩存穿透
緩存穿透是指查詢一個(gè)根本不存在的數(shù)據(jù)随静,緩存層和存儲(chǔ)層都不會(huì)命中八千,通常出于容錯(cuò)的考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存層燎猛,如圖11-3所示整個(gè)過程分為如下3步:
1)緩存層不命中恋捆。
2)存儲(chǔ)層不命中,不將空結(jié)果寫回緩存重绷。
3)返回空結(jié)果沸停。
緩存穿透將導(dǎo)致不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存保護(hù)后端存儲(chǔ)的意義论寨。緩存穿透問題可能會(huì)使后端存儲(chǔ)負(fù)載加大星立,由于很多后端存儲(chǔ)不具備高并發(fā)性,甚至可能造成后端存儲(chǔ)宕掉葬凳。通炒麓梗可以在程序中分別統(tǒng)計(jì)總調(diào)用數(shù)、緩存層命中數(shù)火焰、存儲(chǔ)層命中數(shù)劲装,如果發(fā)現(xiàn)大量存儲(chǔ)層空命中,可能就是出現(xiàn)了緩存穿透問題。
造成緩存穿透的基本原因有兩個(gè):
①自身業(yè)務(wù)代碼或者數(shù)據(jù)出現(xiàn)問題
②一些惡意攻擊占业、爬蟲等造成大量空命中
如何解決緩存穿透問題绒怨,實(shí)際上這是一個(gè)開放問題,有很多解決方法谦疾。下面是兩種典型的解決方案:
當(dāng)存儲(chǔ)層不命中后南蹂,仍然將空對(duì)象保留到緩存層中,之后再訪問這個(gè)數(shù)據(jù)將會(huì)從緩存中獲取念恍,這樣就保護(hù)了后端數(shù)據(jù)源六剥。
下面是緩存空對(duì)象的代碼實(shí)現(xiàn):
String get(String key) {
// 從緩存中獲取數(shù)據(jù)
String cacheValue = cache.get(key);
// 緩存為空
if (StringUtils.isBlank(cacheValue)) {
// 從存儲(chǔ)中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存儲(chǔ)數(shù)據(jù)為空,需要設(shè)置一個(gè)過期時(shí)間(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 緩存非空
return cacheValue;
}
}
緩存空對(duì)象會(huì)有兩個(gè)問題:
①空值做了緩存峰伙,意味著緩存層中存了更多的鍵疗疟,需要更多的內(nèi)存空間(如果是攻擊,問題更嚴(yán)重)瞳氓,比較有效的方法是針對(duì)這類數(shù)據(jù)設(shè)置一個(gè)較短的過期時(shí)間策彤,讓其自動(dòng)剔除。
②緩存層和存儲(chǔ)層的數(shù)據(jù)會(huì)有一段時(shí)間窗口的不一致匣摘,可能會(huì)對(duì)業(yè)務(wù)有一定影響店诗。例如過期時(shí)間設(shè)置為5分鐘,如果此時(shí)存儲(chǔ)層添加了這個(gè)數(shù)據(jù)恋沃,那此段時(shí)間就會(huì)出現(xiàn)緩存層和存儲(chǔ)層數(shù)據(jù)的不一致必搞,此時(shí)可以利用消息系統(tǒng)或者其他方式清除掉緩存層中的空對(duì)象。
在訪問緩存層和存儲(chǔ)層之前囊咏,將存在的key用布隆過濾器提前保存起來,做第一層攔截塔橡。例如:一個(gè)推薦系統(tǒng)有4億個(gè)用戶id梅割,每個(gè)小時(shí)算法工程師會(huì)根據(jù)每個(gè)用戶之前歷史行為計(jì)算出推薦數(shù)據(jù)放到存儲(chǔ)層中,但是最新的用戶由于沒有歷史行為葛家,就會(huì)發(fā)生緩存穿透的行為户辞,為此可以將所有推薦數(shù)據(jù)的用戶做成布隆過濾器。如果布隆過濾器認(rèn)為該用戶id不存在癞谒,那么就不會(huì)訪問存儲(chǔ)層底燎,在一定程度保護(hù)了存儲(chǔ)層。
布隆過濾器的相關(guān)知識(shí)可以參考https://en.wikipedia.org/wiki/Bloom_filter弹砚∷裕可以利用Redis的Bitmaps實(shí)現(xiàn)布隆過濾器,GitHub上已經(jīng)開源了類似的方案桌吃,可以參考:https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter朱沃。這種方法適用于數(shù)據(jù)命中不高、數(shù)據(jù)相對(duì)固定、實(shí)時(shí)性低(通常是數(shù)據(jù)集較大)的應(yīng)用場(chǎng)景逗物,代碼維護(hù)較為復(fù)雜搬卒,但是緩存空間占用少。
(3)兩種方案的對(duì)比
二翎卓、緩存雪崩
由于緩存層承載著大量請(qǐng)求契邀,有效地保護(hù)了存儲(chǔ)層,但是如果緩存層由于某些原因不能提供服務(wù)失暴,于是所有的請(qǐng)求都會(huì)達(dá)到存儲(chǔ)層蹂安,存儲(chǔ)層的調(diào)用量會(huì)暴增,造成存儲(chǔ)層也會(huì)級(jí)聯(lián)宕機(jī)的情況锐帜。緩存雪崩的英文原意是stampeding herd(奔逃的野牛)田盈,指的是緩存層宕掉后,流量會(huì)像奔逃的野牛一樣缴阎,打向后端存儲(chǔ)允瞧。
預(yù)防和解決緩存雪崩問題,可以從以下三個(gè)方面進(jìn)行著手蛮拔。
在項(xiàng)目上線前述暂,演練緩存層宕掉后,應(yīng)用以及后端的負(fù)載情況以及可能出現(xiàn)的問題建炫,在此基礎(chǔ)上做一些預(yù)案設(shè)定畦韭。
和飛機(jī)都有多個(gè)引擎一樣,如果緩存層設(shè)計(jì)成高可用的肛跌,即使個(gè)別節(jié)點(diǎn)艺配、個(gè)別機(jī)器、甚至是機(jī)房宕掉衍慎,依然可以提供服務(wù)转唉,例如前面介紹過的Redis Sentinel和Redis Cluster都實(shí)現(xiàn)了高可用。
無論是緩存層還是存儲(chǔ)層都會(huì)有出錯(cuò)的概率稳捆,可以將它們視同為資源赠法。作為并發(fā)量較大的系統(tǒng),假如有一個(gè)資源不可用乔夯,可能會(huì)造成線程全部阻塞(hang)在這個(gè)資源上砖织,造成整個(gè)系統(tǒng)不可用。降級(jí)機(jī)制在高并發(fā)系統(tǒng)中是非常普遍的:比如推薦服務(wù)中末荐,如果個(gè)性化推薦服務(wù)不可用侧纯,可以降級(jí)補(bǔ)充熱點(diǎn)數(shù)據(jù),不至于造成前端頁面是開天窗鞠评。在實(shí)際項(xiàng)目中茂蚓,我們需要對(duì)重要的資源(例如Redis、MySQL、HBase聋涨、外部接口)都進(jìn)行隔離晾浴,讓每種資源都單獨(dú)運(yùn)行在自己的線程池中,即使個(gè)別資源出現(xiàn)了問題牍白,對(duì)其他服務(wù)沒有影響脊凰。但是線程池如何管理,比如如何關(guān)閉資源池茂腥、開啟資源池狸涌、資源池閥值管理,這些做起來還是相當(dāng)復(fù)雜的最岗。這里推薦一個(gè)Java依賴隔離工具Hystrix帕胆,如圖11-15所示。Hystrix是解決依賴隔離的利器般渡,只適用于Java應(yīng)用懒豹。
事前: 項(xiàng)目上線前提前演練,做好預(yù)案驯用;
事中: ①高可用集群(sentinel/cluster)脸秽,避免緩存全盤崩潰。 ②本地緩存+限流&降級(jí)(hystrix)蝴乔,避免mysql被拖垮记餐。(可以通過限流組件設(shè)置讓多少請(qǐng)求進(jìn)入,比如來了5000請(qǐng)求薇正,讓2000通過片酝,進(jìn)入數(shù)據(jù)庫,剩余請(qǐng)求走降級(jí)铝穷,限流組件會(huì)調(diào)用你自己開發(fā)好的一個(gè)降級(jí)組件钠怯,返回一些默認(rèn)值,友情提示等) 【這樣能保證數(shù)據(jù)庫不會(huì)死曙聂。能保證2/5的請(qǐng)求可以被處理,也就是說用戶點(diǎn)5次可能有幾次刷不出來頁面鞠鲜∧梗】
事后: 借助持久化,快速恢復(fù)贤姆。</textarea>
三榆苞、緩存并發(fā)競(jìng)爭(zhēng)
多客戶端同時(shí)并發(fā)寫一個(gè)key,可能本來應(yīng)該先到的數(shù)據(jù)后到了霞捡,導(dǎo)致數(shù)據(jù)版本錯(cuò)了坐漏。或者是多客戶端同時(shí)獲取一個(gè)key,修改值之后再寫回去赊琳,只要順序錯(cuò)了街夭,數(shù)據(jù)就錯(cuò)了。
解決方法:
1.分布式鎖+時(shí)間戳
主要是使用一個(gè)分布式鎖躏筏,大家去搶鎖板丽,搶到鎖就做set操作。加鎖的目的實(shí)際上就是把并行讀寫改成串行讀寫的方式趁尼,從而來避免資源競(jìng)爭(zhēng)埃碱。
我們線上常用的是zookeeper的分布式鎖,用redis自身的分布式鎖也可以實(shí)現(xiàn)酥泞,但是我們很少這樣做砚殿。
另外,Redis自己就有天然解決這個(gè)問題的CAS類的樂觀鎖方案芝囤∷蒲祝基于redis的分布式鎖主要用到的是setnx。
2.利用消息隊(duì)列
在并發(fā)量過大的情況下,可以通過消息中間件進(jìn)行處理凡人,把并行讀寫進(jìn)行串行化名党。把Redis.set操作放在隊(duì)列中使其串行化,必須的一個(gè)一個(gè)執(zhí)行。這種方式在一些高并發(fā)的場(chǎng)景中算是一種通用的解決方案挠轴。
四传睹、緩存與數(shù)據(jù)庫雙寫一致性
1.Cache-Aside Pattern
最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,Cache-Aside Pattern岸晦,可以參考微軟官網(wǎng)的介紹:Cache-Aside Pattern(翻譯)
Cache-Aside Pattern總結(jié)起來就是下面兩句話:
(1)讀的時(shí)候:先讀緩存欧啤,如果緩存中沒有,就讀數(shù)據(jù)庫启上,然后取出數(shù)據(jù)后放入緩存邢隧,同時(shí)返回響應(yīng)
(2)更新的時(shí)候:先刪除緩存,然后再更新數(shù)據(jù)庫
2.操作緩存時(shí)是先操作數(shù)據(jù)庫還是先操作緩存冈在?操作緩存是刪除緩存倒慧,還是更新緩存?
參考:究竟先操作緩存包券,還是數(shù)據(jù)庫纫谅?
讀緩存的情況,應(yīng)該先讀緩存溅固,再讀數(shù)據(jù)庫付秕,這點(diǎn)是沒有疑問的 。而寫緩存的情況就有點(diǎn)復(fù)雜了侍郭,可以看下面這幅圖询吴。
對(duì)于刪除緩存的方案掠河,當(dāng)緩存刪除后,客戶端如果再次需要訪問相關(guān)數(shù)據(jù)時(shí)猛计,由于緩存中沒有唠摹,就會(huì)通過走數(shù)據(jù)庫,最終再放入緩存中有滑。
另外需要說明的是跃闹,緩存中的數(shù)據(jù)有時(shí)可能就是一張表中的某個(gè)字段的值,而有時(shí)可能是多張表進(jìn)行復(fù)雜計(jì)算后生成的一個(gè)最終值毛好。
3.庫存案例
以庫存服務(wù)來說明說明更新緩存的操作望艺。庫存服務(wù)特點(diǎn)是實(shí)時(shí)性比較高,也會(huì)用到緩存肌访,會(huì)將庫存數(shù)據(jù)放在緩存中找默。
如何保證數(shù)據(jù)庫和緩存中的庫存數(shù)據(jù)的雙寫一致性?
(1)最初級(jí)的解決方案
也就是使用前面提到的:操作緩存時(shí)吼驶,先刪除緩存惩激,在更新數(shù)據(jù)庫。
(2)將數(shù)據(jù)庫與緩存更新與讀取操作進(jìn)行異步串行化
隊(duì)列頭部的寫請(qǐng)求操作可能20ms完成蟹演,之后的讀請(qǐng)求可能會(huì)hang住40ms风钻,之后也可能花費(fèi)20ms完成讀操作。
高并發(fā)的場(chǎng)景下酒请,該解決方案要注意的問題
(1)讀請(qǐng)求長時(shí)阻塞
由于讀請(qǐng)求進(jìn)行了非常輕度的異步化骡技,所以一定要注意讀超時(shí)的問題,每個(gè)讀請(qǐng)求必須在超時(shí)時(shí)間范圍內(nèi)返回
該解決方案羞反,最大的風(fēng)險(xiǎn)點(diǎn)在于說布朦,可能數(shù)據(jù)更新很頻繁,導(dǎo)致隊(duì)列中積壓了大量更新操作在里面昼窗,然后讀請(qǐng)求會(huì)發(fā)生大量的超時(shí)是趴,最后導(dǎo)致大量的請(qǐng)求直接走數(shù)據(jù)庫。
務(wù)必通過一些模擬真實(shí)的測(cè)試澄惊,看看更新數(shù)據(jù)的頻繁是怎樣的
另外一點(diǎn)唆途,因?yàn)橐粋€(gè)隊(duì)列中,可能會(huì)積壓針對(duì)多個(gè)數(shù)據(jù)項(xiàng)的更新操作掸驱,因此需要根據(jù)自己的業(yè)務(wù)情況進(jìn)行測(cè)試窘哈,可能需要部署多個(gè)服務(wù),每個(gè)服務(wù)分?jǐn)傄恍?shù)據(jù)的更新操作
如果一個(gè)內(nèi)存隊(duì)列里居然會(huì)擠壓100個(gè)商品的庫存修改操作亭敢,每隔庫存修改操作要耗費(fèi)10ms區(qū)完成,那么最后一個(gè)商品的讀請(qǐng)求图筹,可能等待10 * 100 = 1000ms = 1s后帅刀,才能得到數(shù)據(jù)让腹,這個(gè)時(shí)候就導(dǎo)致讀請(qǐng)求的長時(shí)阻塞
一定要做根據(jù)實(shí)際業(yè)務(wù)系統(tǒng)的運(yùn)行情況,去進(jìn)行一些壓力測(cè)試扣溺,和模擬線上環(huán)境骇窍,去看看最繁忙的時(shí)候,內(nèi)存隊(duì)列可能會(huì)擠壓多少更新操作锥余,可能會(huì)導(dǎo)致最后一個(gè)更新操作對(duì)應(yīng)的讀請(qǐng)求腹纳,會(huì)hang多少時(shí)間,如果讀請(qǐng)求在200ms返回驱犹,如果你計(jì)算過后嘲恍,哪怕是最繁忙的時(shí)候,積壓10個(gè)更新操作雄驹,最多等待200ms佃牛,那還可以的。
如果一個(gè)內(nèi)存隊(duì)列可能積壓的更新操作特別多医舆,那么你就要加機(jī)器俘侠,讓每個(gè)機(jī)器上部署的服務(wù)實(shí)例處理更少的數(shù)據(jù),那么每個(gè)內(nèi)存隊(duì)列中積壓的更新操作就會(huì)越少
其實(shí)根據(jù)之前的項(xiàng)目經(jīng)驗(yàn)蔬将,一般來說數(shù)據(jù)的寫頻率是很低的爷速,因此實(shí)際上正常來說,在隊(duì)列中積壓的更新操作應(yīng)該是很少的
針對(duì)讀高并發(fā)霞怀,讀緩存架構(gòu)的項(xiàng)目惫东,一般寫請(qǐng)求相對(duì)讀來說,是非常非常少的里烦,每秒的QPS能到幾百就不錯(cuò)了
一秒凿蒜,500的寫操作,5份胁黑,每200ms废封,就100個(gè)寫操作
單機(jī)器,20個(gè)內(nèi)存隊(duì)列丧蘸,每個(gè)內(nèi)存隊(duì)列漂洋,可能就積壓5個(gè)寫操作,每個(gè)寫操作性能測(cè)試后力喷,一般在20ms左右就完成
那么針對(duì)每個(gè)內(nèi)存隊(duì)列中的數(shù)據(jù)的讀請(qǐng)求刽漂,也就最多hang一會(huì)兒,200ms以內(nèi)肯定能返回了
寫QPS擴(kuò)大10倍弟孟,但是經(jīng)過剛才的測(cè)算贝咙,就知道,單機(jī)支撐寫QPS幾百?zèng)]問題拂募,那么就擴(kuò)容機(jī)器庭猩,擴(kuò)容10倍的機(jī)器窟她,10臺(tái)機(jī)器,每個(gè)機(jī)器20個(gè)隊(duì)列蔼水,200個(gè)隊(duì)列
大部分的情況下震糖,應(yīng)該是這樣的,大量的讀請(qǐng)求過來趴腋,都是直接走緩存取到數(shù)據(jù)的
少量情況下吊说,可能遇到讀跟數(shù)據(jù)更新沖突的情況,如上所述优炬,那么此時(shí)更新操作如果先入隊(duì)列颁井,之后可能會(huì)瞬間來了對(duì)這個(gè)數(shù)據(jù)大量的讀請(qǐng)求,但是因?yàn)樽隽巳ブ氐膬?yōu)化穿剖,所以也就一個(gè)更新緩存的操作跟在它后面
等數(shù)據(jù)更新完了蚤蔓,讀請(qǐng)求觸發(fā)的緩存更新操作也完成,然后臨時(shí)等待的讀請(qǐng)求全部可以讀到緩存中的數(shù)據(jù)
(2)讀請(qǐng)求并發(fā)量過高
這里還必須做好壓力測(cè)試糊余,確保恰巧碰上上述情況的時(shí)候秀又,還有一個(gè)風(fēng)險(xiǎn),就是突然間大量讀請(qǐng)求會(huì)在幾十毫秒的延時(shí)hang在服務(wù)上贬芥,看服務(wù)能不能抗的住吐辙,需要多少機(jī)器才能抗住最大的極限情況的峰值
但是因?yàn)椴⒉皇撬械臄?shù)據(jù)都在同一時(shí)間更新,緩存也不會(huì)同一時(shí)間失效蘸劈,所以每次可能也就是少數(shù)數(shù)據(jù)的緩存失效了昏苏,然后那些數(shù)據(jù)對(duì)應(yīng)的讀請(qǐng)求過來,并發(fā)量應(yīng)該也不會(huì)特別大
按1:99的比例計(jì)算讀和寫的請(qǐng)求威沫,每秒5萬的讀QPS贤惯,可能只有500次更新操作
如果一秒有500的寫QPS,那么要測(cè)算好棒掠,可能寫操作影響的數(shù)據(jù)有500條孵构,這500條數(shù)據(jù)在緩存中失效后,可能導(dǎo)致多少讀請(qǐng)求烟很,發(fā)送讀請(qǐng)求到庫存服務(wù)來颈墅,要求更新緩存
一般來說,1:1雾袱,1:2恤筛,1:3,每秒鐘有1000個(gè)讀請(qǐng)求芹橡,會(huì)hang在庫存服務(wù)上毒坛,每個(gè)讀請(qǐng)求最多hang多少時(shí)間,200ms就會(huì)返回
在同一時(shí)間最多hang住的可能也就是單機(jī)200個(gè)讀請(qǐng)求林说,同時(shí)hang住
單機(jī)hang200個(gè)讀請(qǐng)求粘驰,還是ok的 1:20屡谐,每秒更新500條數(shù)據(jù),這500秒數(shù)據(jù)對(duì)應(yīng)的讀請(qǐng)求蝌数,會(huì)有20 * 500 = 1萬
1萬個(gè)讀請(qǐng)求全部hang在庫存服務(wù)上,就死定了
(3)【多服務(wù)實(shí)例部署的請(qǐng)求路由】
可能這個(gè)服務(wù)部署了多個(gè)實(shí)例度秘,那么必須保證執(zhí)行數(shù)據(jù)更新操作顶伞,以及執(zhí)行緩存更新操作的請(qǐng)求,都通過nginx服務(wù)器路由到相同的服務(wù)實(shí)例上
(4)熱點(diǎn)商品的路由問題剑梳,導(dǎo)致請(qǐng)求的傾斜
萬一某個(gè)商品的讀寫請(qǐng)求特別高唆貌,全部打到相同的機(jī)器的相同的隊(duì)列里面去了,可能造成某臺(tái)機(jī)器的壓力過大
就是說垢乙,因?yàn)橹挥性谏唐窋?shù)據(jù)更新的時(shí)候才會(huì)清空緩存锨咙,然后才會(huì)導(dǎo)致讀寫并發(fā),所以更新頻率不是太高的話追逮,這個(gè)問題的影響并不是特別大
但是的確可能某些機(jī)器的負(fù)載會(huì)高一些