引入緩存竟然會(huì)帶來(lái)這么多問(wèn)題?
哈嘍,大家好呀,我是呼嚕嚕,最近很忙好久沒(méi)更新了,今天我們通過(guò)緩存與數(shù)據(jù)庫(kù)之間的一致性這個(gè)老生常談的問(wèn)題來(lái)切入,聊聊如何合理的設(shè)計(jì)一個(gè)緩存系統(tǒng)?
如今互聯(lián)網(wǎng)應(yīng)用,無(wú)論是web還是app,都基本遵循"前端-后端-數(shù)據(jù)庫(kù)"的架構(gòu)模型
當(dāng)業(yè)務(wù)處于起步階段,流量比較小的時(shí)候,上述能夠支撐;但隨著業(yè)務(wù)的擴(kuò)張,用戶(hù)數(shù)和流量越來(lái)越大,也就需要整個(gè)架構(gòu)支撐起更大的并發(fā)量,但我們服務(wù)器上的資源總是有限的,當(dāng)每天流量達(dá)到高峰時(shí),往往這個(gè)時(shí)候數(shù)據(jù)庫(kù)最先頂不住
當(dāng)我們分析這些互聯(lián)網(wǎng)應(yīng)用的流量時(shí)候,發(fā)現(xiàn)大部分的流量實(shí)際上都是讀請(qǐng)求,而且大部分?jǐn)?shù)據(jù)并沒(méi)有頻繁被改變**(即讀多寫(xiě)少場(chǎng)景,注意本文全文討論的方案都是基于這個(gè)前提**)。這個(gè)時(shí)候引入緩存,是提升性能的一種行之有效的方式,緩存在計(jì)算機(jī)的世界中處處可見(jiàn),比如CPU緩存,瀏覽器緩存,操作系統(tǒng)緩存,程序代碼中自定義緩存
由于數(shù)據(jù)庫(kù)每秒能接受的請(qǐng)求次數(shù)QPS是有限的,當(dāng)我們?cè)跀?shù)據(jù)庫(kù)前面,引入緩存來(lái)充當(dāng)緩沖層;如果命中緩存就直接獲取目標(biāo)數(shù)據(jù)并返回,不僅能減少對(duì)數(shù)據(jù)庫(kù)的直接訪問(wèn)帶來(lái)的計(jì)算壓力,還能提升響應(yīng)速度,充分壓榨有效的資源,其本質(zhì)是額外消耗更高速的空間來(lái)?yè)Q時(shí)間
圖片
凡是有利有弊,引入緩存后,享受緩存帶來(lái)的種種好處的優(yōu)點(diǎn),但緩存系統(tǒng)其實(shí)是非常復(fù)雜的,緩存和數(shù)據(jù)庫(kù)的一致性也是個(gè)繞不開(kāi),讓人腦闊疼的問(wèn)題;還需要考慮緩存的穩(wěn)定性、命中率、熱點(diǎn)數(shù)據(jù)、過(guò)期時(shí)間等等,我們下文慢慢道來(lái)
本地緩存、分布式緩存
緩存有各種分類(lèi),常見(jiàn)的是與應(yīng)用耦合程度劃分為:本地緩存local cache和分布式緩存remote cache
本地緩存
本地緩存,由于存在于應(yīng)用程序的本地內(nèi)存,應(yīng)用和緩存在同一個(gè)進(jìn)程內(nèi),且沒(méi)有網(wǎng)絡(luò)延遲,所以速度快
但本地緩存的大小通常受到物理內(nèi)存的限制,而且還要兼顧應(yīng)用程序正常運(yùn)行,容量有限,擴(kuò)展性差,無(wú)法輕松擴(kuò)展到多個(gè)節(jié)點(diǎn)。還有就是多個(gè)應(yīng)用實(shí)例下無(wú)法直接的共享緩存,數(shù)據(jù)的一致性難以保證,復(fù)雜度高。數(shù)據(jù)會(huì)隨著應(yīng)用程序的重啟而丟失
適合讀寫(xiě)密集、對(duì)數(shù)據(jù)一致性要求較低、網(wǎng)絡(luò)環(huán)境不穩(wěn)定的場(chǎng)景
分布式緩存
主要是指與應(yīng)用分離的獨(dú)立緩存組件,比如redis,可擴(kuò)展性強(qiáng),容量大,可以通過(guò)集群水平擴(kuò)展;通過(guò)通過(guò)一致性哈希等技術(shù),保證多節(jié)點(diǎn)之間的數(shù)據(jù)一致性,而且都集成好了,開(kāi)發(fā)者一般直接使用這些特性
當(dāng)然由于存在網(wǎng)絡(luò)延遲,與本地緩存相比,速度較慢;硬件成本也需要較高,來(lái)保證其高可用、高可靠性
更適合電商平臺(tái)、社交網(wǎng)絡(luò)等流量并發(fā)大的平臺(tái),或者互聯(lián)網(wǎng)這種隨著業(yè)務(wù)增長(zhǎng),需要彈性擴(kuò)展以滿(mǎn)足需求的場(chǎng)景
還有綜合二者特點(diǎn)的多級(jí)緩存,將本地緩存和分布式緩存結(jié)合起來(lái),本地緩存作為一級(jí)緩存,存儲(chǔ)更新頻率低,訪問(wèn)頻率高數(shù)據(jù);分布式緩存作為二級(jí)緩存,存儲(chǔ)更新頻率很高的數(shù)據(jù)
當(dāng)用戶(hù)獲取數(shù)據(jù)時(shí),先從一級(jí)緩存中獲取數(shù)據(jù),如果一級(jí)緩存有數(shù)據(jù)則返回?cái)?shù)據(jù),否則從二級(jí)緩存中獲取數(shù)據(jù)。如果二級(jí)緩存中有數(shù)據(jù)則更新一級(jí)緩存,然后將數(shù)據(jù)返回客戶(hù)端。如果二級(jí)緩存沒(méi)有數(shù)據(jù)則去數(shù)據(jù)庫(kù)查詢(xún)數(shù)據(jù),然后更新二級(jí)緩存,接著再更新一級(jí)緩存,最后將數(shù)據(jù)返回給客戶(hù)端。這里邏輯其實(shí)和CPU內(nèi)部的緩存很像,大家感興趣地可以自行查閱筆者之前的一篇文章-CPU緩存
但緩存相關(guān)的問(wèn)題邏輯挑戰(zhàn),無(wú)論本地緩存還是分布式緩存都是一樣的,為方便起見(jiàn),本文將全文以redis為例,來(lái)代稱(chēng)緩存
緩存穿透、緩存擊穿、緩存雪崩
在將緩存和數(shù)據(jù)庫(kù)的一致性之前,我們需要保證,引入的緩存,即構(gòu)建的緩存系統(tǒng)是穩(wěn)定的,這是保證數(shù)據(jù)一致性的前提
關(guān)于緩存的穩(wěn)定性,有3種經(jīng)典問(wèn)題:緩存穿透、緩存擊穿、緩存雪崩,聊這3個(gè)問(wèn)題前,我們得知曉緩存最常見(jiàn)的應(yīng)用模式Cache-Aside Pattern旁路緩存的讀模式:
圖片
旁路緩存模式,是指優(yōu)先查詢(xún)緩存,查詢(xún)不到再去查詢(xún)數(shù)據(jù)庫(kù)。如果這時(shí)候數(shù)據(jù)庫(kù)查到數(shù)據(jù)了,就將緩存的數(shù)據(jù)回寫(xiě)更新,這樣緩存可以為后續(xù)請(qǐng)求服務(wù)!
緩存穿透
緩存穿透: 當(dāng)請(qǐng)求過(guò)來(lái),訪問(wèn)不存在的數(shù)據(jù)時(shí)(即既不在緩存中,也不在數(shù)據(jù)庫(kù)中),這會(huì)導(dǎo)致訪問(wèn)緩存,未命中,繼續(xù)訪問(wèn)數(shù)據(jù)庫(kù)db,然后發(fā)現(xiàn)在數(shù)據(jù)庫(kù)中還是未查詢(xún)到數(shù)據(jù),這個(gè)時(shí)候也就不能回寫(xiě)緩存,來(lái)為后續(xù)的請(qǐng)求服務(wù);也就是說(shuō),當(dāng)這種請(qǐng)求過(guò)來(lái),每次都會(huì)去查數(shù)據(jù)庫(kù),緩存形同虛設(shè),一旦流量暴增,容易直接帶崩數(shù)據(jù)庫(kù)
圖片
這種不存在的數(shù)據(jù)可能被管理員誤刪,也有可能被黑客惡意利用(惡意請(qǐng)求),不斷地去試,一旦發(fā)現(xiàn)一個(gè)不存在的數(shù)據(jù),就拼命發(fā)請(qǐng)求訪問(wèn)這個(gè)數(shù)據(jù),直到數(shù)據(jù)庫(kù)鎖住
那解決辦法也很簡(jiǎn)單,常見(jiàn)的有:
- 比如每次訪問(wèn)數(shù)據(jù)如果既不在緩存中,也不在數(shù)據(jù)庫(kù)中,那就緩存一個(gè)占位符或者空值,過(guò)期時(shí)間也不要設(shè)置過(guò)長(zhǎng),比如1分鐘就行,這樣的話(huà),在1分鐘內(nèi),這么多請(qǐng)求只有一次能直接訪問(wèn)數(shù)據(jù)庫(kù),這樣就能顯著降低數(shù)據(jù)庫(kù)的壓力;如果緩存過(guò)期時(shí)間過(guò)長(zhǎng),會(huì)出現(xiàn)大量的空緩存,進(jìn)而導(dǎo)致緩存資源的浪費(fèi)
- 還可以針對(duì)請(qǐng)求攜帶的參數(shù),比如是那種特殊字符、非法字符等,我們數(shù)據(jù)庫(kù)肯定不會(huì)存這些東西,直接在應(yīng)用服務(wù)層進(jìn)行限制,不允許訪問(wèn)
- 還可以通過(guò)第三方組件來(lái)實(shí)現(xiàn),比如布隆過(guò)濾器,其主要是其特性:布隆過(guò)濾器判斷一個(gè)元素不在集合中,那肯定就不在。如果判斷存在,那有一定可能性它在說(shuō)謊,具體原理可以參考筆者以前的一篇文章海量數(shù)據(jù)處理的利器-布隆過(guò)濾器。在緩存和數(shù)據(jù)庫(kù)之間再加上布隆過(guò)濾器,通過(guò)布隆過(guò)濾器快速判斷數(shù)據(jù)是否存在,從而避免多次之間請(qǐng)求數(shù)據(jù)庫(kù)
緩存擊穿
在我們正常的業(yè)務(wù)之中,總有一些數(shù)據(jù)會(huì)被頻繁訪問(wèn),這就是熱點(diǎn)數(shù)據(jù)
所謂的緩存擊穿指的是,緩存中熱點(diǎn)數(shù)據(jù)的key過(guò)期失效,由于是熱點(diǎn)數(shù)據(jù),在過(guò)期的一瞬間會(huì)有大量的請(qǐng)求過(guò)來(lái)(高并發(fā)),這些請(qǐng)求,最終都會(huì)直接訪問(wèn)數(shù)據(jù)庫(kù),這樣數(shù)據(jù)庫(kù)很容易被打垮,緩存仿佛被"擊穿"了
常見(jiàn)的解決方案:
- 加鎖,進(jìn)程鎖/分布式鎖,當(dāng)請(qǐng)求過(guò)來(lái)時(shí),緩存未命中時(shí),會(huì)通過(guò)鎖將這個(gè)緩存key鎖上,等當(dāng)這個(gè)請(qǐng)求從數(shù)據(jù)庫(kù)獲取數(shù)據(jù)后再回寫(xiě)到緩存中后,再釋放鎖;期間其他請(qǐng)求過(guò)來(lái),會(huì)獲取鎖失敗,等待一段時(shí)間重試,就可以直接讀取緩存了。需要注意的是,如果業(yè)務(wù)量不大,進(jìn)程鎖就夠了的話(huà),也就沒(méi)必要上分布式鎖,多引入額外組件,就會(huì)增加系統(tǒng)的不穩(wěn)定性
圖片
還可以繼續(xù)改進(jìn),將請(qǐng)求2未獲得鎖,直接返回,升級(jí)成自旋鎖,它不直接返回,而是等待一會(huì)重新嘗試獲取鎖,這種高并發(fā)情況下,只有唯一請(qǐng)求是db請(qǐng)求,所有請(qǐng)求共享結(jié)果
- 給緩存的Key設(shè)置合理的過(guò)期時(shí)間并加上隨機(jī)值,盡量減少緩存短期大量失效,出現(xiàn)大量訪問(wèn)數(shù)據(jù)庫(kù)的情況,實(shí)現(xiàn)"削峰填谷"
- 網(wǎng)上有文章提出,可以讓熱點(diǎn)數(shù)據(jù)的緩存不設(shè)置過(guò)期時(shí)間,這樣不就可以永不過(guò)期嘛,但這其實(shí)是個(gè)很危險(xiǎn)的操作
使用緩存的前提是一定要設(shè)置過(guò)期時(shí)間,因?yàn)橛捎陧?xiàng)目會(huì)不斷迭代更新,業(yè)務(wù)不斷復(fù)雜,開(kāi)發(fā)人員更替,緩存會(huì)變得越來(lái)越難以維護(hù),另外緩存和數(shù)據(jù)庫(kù)無(wú)法避免的數(shù)據(jù)不一致的情況,緩存的過(guò)期時(shí)間其實(shí)就是兜底,防止緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)長(zhǎng)時(shí)間不一致
我們還可以通過(guò)消息隊(duì)列來(lái)間接地讓熱點(diǎn)數(shù)據(jù)的緩存延期,當(dāng)熱點(diǎn)緩存過(guò)期時(shí),后臺(tái)服務(wù)再檢測(cè)更新緩存,防止緩存擊穿;至于是否延期,得做訪問(wèn)量分析與統(tǒng)計(jì),當(dāng)然引入新的組件也會(huì)帶來(lái)額外的穩(wěn)定性問(wèn)題,還是得根據(jù)業(yè)務(wù)情況,實(shí)事求是
緩存雪崩
緩存雪崩,指定是大量請(qǐng)求未命中緩存,直接訪問(wèn)數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)壓力過(guò)大,倘若請(qǐng)求足夠的多,會(huì)直接將數(shù)據(jù)庫(kù)壓垮,繼而影響整個(gè)系統(tǒng),如同"雪崩"
個(gè)人感覺(jué)緩存擊穿是緩存雪崩的一個(gè)子集,緩存雪崩一般有2種誘因:緩存服務(wù)異常,比如redis故障宕機(jī)或者緩存服務(wù)是正常的,但大量緩存數(shù)據(jù)在同一時(shí)間過(guò)期
一般解決redis故障宕機(jī),是搭建集群,由單節(jié)點(diǎn)到多節(jié)點(diǎn),提升redis的容災(zāi)能力,當(dāng)主節(jié)點(diǎn)宕機(jī)后,從節(jié)點(diǎn)可以切換成為主節(jié)點(diǎn),繼續(xù)提供緩存服務(wù);若是真的宕機(jī)了,那我們應(yīng)該使用熔斷機(jī)制,同時(shí)當(dāng)流量到達(dá)一定的閾值,直接禁止請(qǐng)求對(duì)數(shù)據(jù)庫(kù)的訪問(wèn),返回系統(tǒng)擁擠之類(lèi)的提示,維持系統(tǒng)穩(wěn)定,等待緩存恢復(fù)再允許對(duì)數(shù)據(jù)庫(kù)訪問(wèn)
防止大量緩存數(shù)據(jù)在同一時(shí)間過(guò)期,一般是給緩存的Key設(shè)置合理的過(guò)期時(shí)間并加上隨機(jī)偏差,盡量讓緩存失效時(shí)間均勻分布,實(shí)現(xiàn)"削峰填谷",簡(jiǎn)單而有效
圖片
要么加鎖,唯一db請(qǐng)求,所有同類(lèi)請(qǐng)求共享結(jié)果,與緩存擊穿的解決方法一致,我們就不再贅述了
還有一種方式就是,當(dāng)每天系統(tǒng)訪問(wèn)的流量高峰來(lái)臨之前,先提前將熱點(diǎn)數(shù)據(jù)入緩存,避免直到用戶(hù)請(qǐng)求的時(shí)候,再先查詢(xún)數(shù)據(jù)庫(kù),然后將數(shù)據(jù)緩存的過(guò)程,這個(gè)也叫緩存預(yù)熱
CAP原則 和 如何保證緩存一致性
由于在數(shù)據(jù)庫(kù)層前,引入緩存,主要是通過(guò)空間去換時(shí)間,享受緩存帶來(lái)的種種好處的優(yōu)點(diǎn),但此時(shí)一份數(shù)據(jù)存在不同的副本,且在不同空間中,此時(shí)更新緩存、db就會(huì)帶來(lái)緩存一致性的挑戰(zhàn)
我們還需要了解一下著名的CAP原則,指在一個(gè)分布式系統(tǒng)中,一致性Consistency、可用性Availability、分區(qū)容錯(cuò)性Partition tolerance,這3者最多同時(shí)滿(mǎn)足2項(xiàng),不可能同時(shí)滿(mǎn)足3項(xiàng)!?。?/p>
圖片
- 一致性Consistency,即所有節(jié)點(diǎn)在同一時(shí)間具有相同的數(shù)據(jù),強(qiáng)一致性
- 可用性Availability,即服務(wù)必須一直處于可用的狀態(tài),每次請(qǐng)求都能獲取到正常的響應(yīng),高可用
- 分區(qū)容錯(cuò)性Partition tolerance,即分區(qū)故障時(shí),要求在一定時(shí)限內(nèi),仍然或者恢復(fù)到能對(duì)外提供滿(mǎn)足一致性和可用性的服務(wù),系統(tǒng)繼續(xù)正常運(yùn)行
還記得本文的一開(kāi)始嗎?
為了應(yīng)對(duì)高流量,我們的系統(tǒng)選擇了高性能和高吞吐量,所以只能滿(mǎn)足AP。
而緩存與數(shù)據(jù)庫(kù)的緩存一致性難以避免的具體原因是:由于無(wú)法保證同時(shí)更新db和緩存不在同一個(gè)事務(wù)中,所以其不是原子操作,緩存不一致是無(wú)法避免的!
圖片
要保證強(qiáng)一致性,我們可以上分布式鎖,但會(huì)導(dǎo)致整個(gè)系統(tǒng)的并發(fā)性能下降,還記得我們引入緩存的初衷嗎?是為了提升系統(tǒng)的整體性能吶?。?!所以這種方案我們一般不采用~
但我們可以通過(guò)一些方案,來(lái)實(shí)現(xiàn)緩存的最終一致性,其次盡可能減小緩存不一致的時(shí)間窗口,我們下面分別來(lái)聊聊常見(jiàn)的幾種方式及其它們的問(wèn)題:
- 先更新數(shù)據(jù)庫(kù),再更新緩存
- 先更新緩存,再更新數(shù)據(jù)庫(kù)
- 先刪緩存,再更新數(shù)據(jù)庫(kù)
- 先更新數(shù)據(jù)庫(kù),再刪除緩存
先更新數(shù)據(jù)庫(kù),再更新緩存
先更新數(shù)據(jù)庫(kù),再更新緩存,可能會(huì)遇到下面這種情況:
圖片
當(dāng)請(qǐng)求(或者可以說(shuō)線程)并發(fā)的情況,比如2個(gè)請(qǐng)求1、2同時(shí)去更新db時(shí),請(qǐng)求1快一點(diǎn);但當(dāng)程序延遲或者其他情況,導(dǎo)致當(dāng)請(qǐng)求去更新緩存時(shí),請(qǐng)求2快一點(diǎn),這就會(huì)導(dǎo)致最終db=20,緩存=10這種數(shù)據(jù)不一致的情況,不一致的情況將持續(xù)到下次緩存失效,或者去更新數(shù)據(jù)庫(kù)緩存的時(shí)候,在此期間還不能保證更新緩存一定就可以成功
先更新緩存,再更新數(shù)據(jù)庫(kù)
這種和先更新數(shù)據(jù)庫(kù),再更新緩存是類(lèi)似的情況:
圖片
這種更新緩存的方式,是無(wú)法避免并發(fā)導(dǎo)致的數(shù)據(jù)不一致問(wèn)題,而且出現(xiàn)的頻率也不低,所以我們應(yīng)該盡量不更新緩存。
先刪緩存,再更新數(shù)據(jù)庫(kù) 和 延遲雙刪
前一個(gè)更新請(qǐng)求,先刪除緩存,再更新數(shù)據(jù)庫(kù),當(dāng)后面讀請(qǐng)求來(lái)發(fā)現(xiàn)沒(méi)有命中緩存,去數(shù)據(jù)庫(kù)讀數(shù)據(jù),然后再回寫(xiě)到緩存中,給后續(xù)請(qǐng)求服務(wù),這是個(gè)很不錯(cuò)的設(shè)想,但它還是會(huì)出現(xiàn)下面這種情況:
圖片
當(dāng)2個(gè)并發(fā)請(qǐng)求過(guò)來(lái),請(qǐng)求1是更新請(qǐng)求,當(dāng)請(qǐng)求1刪除調(diào)緩存后,還沒(méi)去db更新數(shù)據(jù),期間請(qǐng)求2來(lái)獲取數(shù)據(jù),緩存未命中(剛被請(qǐng)求1刪了嘛),去數(shù)據(jù)庫(kù)獲取數(shù)據(jù)10后,后回寫(xiě)緩存,把緩存更新為10;這個(gè)時(shí)候請(qǐng)求1終于去更新db了,把db更新為20,這個(gè)時(shí)候還是會(huì)出現(xiàn)緩存和數(shù)據(jù)庫(kù)不一致的情況
一旦發(fā)生數(shù)據(jù)不一致,臟數(shù)據(jù)會(huì)一直在緩存中,直到下一次更新請(qǐng)求過(guò)來(lái)
補(bǔ)充:延遲雙刪關(guān)注我,我再多講幾句~如今在先刪緩存,再更新數(shù)據(jù)庫(kù)的基礎(chǔ)上,還有個(gè)優(yōu)化版叫延遲雙刪
既然請(qǐng)求可能會(huì)把臟數(shù)據(jù)重新寫(xiě)入緩存中,臟數(shù)據(jù)會(huì)一直在緩存中,直到下一次更新請(qǐng)求過(guò)來(lái),這個(gè)數(shù)據(jù)不一致的時(shí)間窗口較長(zhǎng),如果這個(gè)時(shí)候休眠指定時(shí)間N,我們另起線程(異步化)去刪除這個(gè)臟數(shù)據(jù)緩存,這個(gè)時(shí)候不就能縮短極端情況下不一致的時(shí)間窗口了嘛,一般N設(shè)為5s左右,需要根據(jù)項(xiàng)目實(shí)際情況而定。
另外也可以通過(guò)消息隊(duì)列MQ來(lái)刪除緩存,利用消息隊(duì)列的可靠性,來(lái)保證刪除緩存的操作能夠成功執(zhí)行,并異步化進(jìn)行復(fù)雜邏輯的解耦
先更新數(shù)據(jù)庫(kù),再刪除緩存
那先更新數(shù)據(jù)庫(kù),再刪除緩存呢?它也被稱(chēng)為Cache Aside Pattern旁路緩存的寫(xiě)模式,我們?cè)賮?lái)看一種情況:
圖片
從上面時(shí)序圖,我們可以看出,先更新數(shù)據(jù)庫(kù),再刪除緩存這種方案是可以保證緩存的最終一致性,但它在某一時(shí)間內(nèi),還是存在緩存不一致的時(shí)間窗口(上圖請(qǐng)求2命中緩存與數(shù)據(jù)庫(kù)不一致)
但這個(gè)不一致的時(shí)間窗口很短,通常不超過(guò)1ms,在互聯(lián)網(wǎng)項(xiàng)目中通??梢院雎赃@么短時(shí)間的不一致
但你覺(jué)得這就是終極方案了?
別急我們?cè)倏此锌赡馨l(fā)生的一種情況:
圖片
當(dāng)2個(gè)并發(fā)請(qǐng)求過(guò)來(lái),請(qǐng)求1是讀請(qǐng)求,正好緩存不存在,直接讀取db=20,在回寫(xiě)緩存期間,請(qǐng)求2又過(guò)來(lái)更新db=10,在刪除緩存(沒(méi)緩存),然后請(qǐng)求1再姍姍來(lái)遲地更新緩存=20,這就導(dǎo)致了緩存與數(shù)據(jù)的不一致情況
但實(shí)際上這種情況,觸發(fā)的概率非常低,因?yàn)榫彺娴拇嫒∷俣?內(nèi)存),要遠(yuǎn)遠(yuǎn)快于數(shù)據(jù)庫(kù)(磁盤(pán))。關(guān)于儲(chǔ)存介質(zhì)的速度差異,感興趣地可以去看看計(jì)算機(jī)儲(chǔ)存器的讀寫(xiě)速度差異
所以很難出現(xiàn)請(qǐng)求1已經(jīng)更新了數(shù)據(jù)庫(kù)并且刪除了緩存,請(qǐng)求2才更新完緩存的情況;為防止刪除緩存失敗,給緩存加個(gè)過(guò)期時(shí)間簡(jiǎn)單而有效
但這其實(shí)也反映了:
- 先更新數(shù)據(jù)庫(kù),再刪除緩存這種模式并不太適合寫(xiě)請(qǐng)求遠(yuǎn)遠(yuǎn)多于讀請(qǐng)求的場(chǎng)景下,而且當(dāng)并發(fā)量特別高的情況下,緩存刪除的代價(jià)也會(huì)較大(容易緩存擊穿),這個(gè)時(shí)候更新數(shù)據(jù)庫(kù)后更新緩存可能是更適合的方案,還能進(jìn)而通過(guò)MQ異步來(lái)優(yōu)化
- 如果讀請(qǐng)求遠(yuǎn)遠(yuǎn)大于寫(xiě)請(qǐng)求的場(chǎng)景下,先更新數(shù)據(jù)庫(kù),再刪除緩存是個(gè)較好的方案,背后是lazy計(jì)算的思想:不要每次都重新做復(fù)雜的計(jì)算,而是等到它需要用的時(shí)候再重新計(jì)算
- 本文提到的這4種方案,無(wú)論是哪種方案都是無(wú)法絕對(duì)保證緩存的一致性,只能保證最終一致性,縮短不一致的時(shí)間窗口。所以緩存必須要設(shè)置過(guò)期時(shí)間,這就是對(duì)緩存不一致的兜底措施
- 最后如果對(duì)數(shù)據(jù)一致性要求極高的話(huà),就不要再額外引入緩存,不引入緩存就沒(méi)有這么多煩惱!
如何保證刪除緩存能執(zhí)行成功
另外在實(shí)際環(huán)境中,執(zhí)行刪除緩存,也會(huì)有問(wèn)題,因?yàn)闊o(wú)法保證系統(tǒng)會(huì)一定去刪除緩存,如果刪除緩存失敗,也會(huì)造成緩存與數(shù)據(jù)庫(kù)的不一致,下面介紹幾種常見(jiàn)的方案:
基于消息隊(duì)列刪除緩存
由于刪除緩存不一定能成功,一般會(huì)采用多次重試刪除的方案,需要一個(gè)隊(duì)列來(lái)記錄,是否刪除成功,如果沒(méi)有成功就繼續(xù)回隊(duì)列中,一般會(huì)引入中間件消息隊(duì)列MQ來(lái),利用其高可靠性來(lái)保證刪除操作的執(zhí)行,同時(shí)還能異步化,實(shí)現(xiàn)復(fù)雜業(yè)務(wù)邏輯的解耦
我們來(lái)看下其主要流程:
更新數(shù)據(jù)庫(kù)的同時(shí),發(fā)送刪除緩存的消息到消息隊(duì)列中,首次消費(fèi)消息去執(zhí)行刪除緩存的操作,如果成功就直接返回業(yè)務(wù),并把這個(gè)消息消費(fèi)掉;如果由于各種原因?qū)е戮彺鎰h除失敗,那就重新將這個(gè)消息放進(jìn)消息隊(duì)列中,等待下一次的消費(fèi)
當(dāng)?shù)诙蜗M(fèi)刪除該緩存的消息時(shí),如果刪除成功就把該消息消費(fèi)掉,并返回;如果沒(méi)有刪除成功就繼續(xù)放回消息隊(duì)列中,每個(gè)消息都有消費(fèi)次數(shù)的上限,超出就報(bào)錯(cuò)告警
圖片
另外一般將更新數(shù)據(jù)庫(kù)的模塊和同時(shí)發(fā)生刪除緩存消息的模塊放在同一個(gè)服務(wù)里,因?yàn)檫@樣后期維護(hù)起來(lái),才不會(huì)發(fā)現(xiàn)莫名奇妙,不然就是給排查和維護(hù)上強(qiáng)度~~
當(dāng)然再引入mq,也要額外考慮mq的高可用性,所以需要根據(jù)實(shí)際情況,考慮是否有必要引入mq,如果不引入怎么辦?最簡(jiǎn)單的我們可以通過(guò)內(nèi)存隊(duì)列、線程池等方式實(shí)現(xiàn),性能更高,畢竟在本地沒(méi)有網(wǎng)絡(luò)延遲,代價(jià)就是更考驗(yàn)程序員的心智,啥都要操心~
基于binlog來(lái)刪除緩存
還有一種比較有意思的方式,我們上面需要在程序中顯式去發(fā)送消息,講人話(huà)就是程序需要額外承擔(dān)發(fā)送消息的壓力, 而通過(guò)訂閱數(shù)據(jù)庫(kù)比如Mysql的binlog,來(lái)監(jiān)聽(tīng)數(shù)據(jù)的真實(shí)變化來(lái)直接去處理有關(guān)的緩存,讓程序?qū)P牡厝ゲ僮鲾?shù)據(jù)庫(kù)
binlog用于記錄數(shù)據(jù)庫(kù)執(zhí)行的寫(xiě)入性操作(不包括查詢(xún))信息,以二進(jìn)制的形式保存在磁盤(pán)中。binlog是mysql的邏輯日志,并且由Server層進(jìn)行記錄,使用任何存儲(chǔ)引擎的mysql數(shù)據(jù)庫(kù)都會(huì)記錄binlog日志。可通過(guò)解析binlog文件來(lái)查看數(shù)據(jù)庫(kù)的操作歷史記錄
業(yè)內(nèi)比較成熟的有中間件Canal,我司也用的這個(gè),Canal會(huì)模擬MySQL主從復(fù)制的交互協(xié)議,把自己偽裝成一個(gè) MySQL 的從節(jié)點(diǎn),向MySQL主節(jié)點(diǎn)發(fā)送dump請(qǐng)求,MySQL收到請(qǐng)求后,就會(huì)開(kāi)始推送Binlog給Canal,Canal解析Binlog字節(jié)流,解析出其中有關(guān)數(shù)據(jù)庫(kù)中數(shù)據(jù)更新的日志,解析日志并執(zhí)行對(duì)應(yīng)數(shù)據(jù)的刪除緩存操作,然后再引入MQ,通過(guò)消息隊(duì)列的ACK機(jī)制,來(lái)確保這條消息的執(zhí)行成功
圖片
關(guān)注我,小牛呼嚕嚕,我再說(shuō)幾句:
希望大家通過(guò)這些方案的學(xué)習(xí),能夠領(lǐng)悟?yàn)槭裁粗荒軡M(mǎn)足AP?
為什么緩存的數(shù)據(jù)一致性問(wèn)題是無(wú)法避免的挑戰(zhàn)?
引入緩存后,我們?cè)撊绾伪O(jiān)控起來(lái)呢?進(jìn)一步分析過(guò)期時(shí)間是否合適,緩存的命中率
或者是否必需引入緩存?不引入緩存可就沒(méi)有緩存的數(shù)據(jù)一致性,這些都需要數(shù)據(jù)分析作為支撐
或者引入緩存如何進(jìn)一步優(yōu)化,緩存的key如何花式設(shè)置,緩存預(yù)熱有講究,還有團(tuán)隊(duì)如何規(guī)范使用緩存等等,有太多可以深究