本文轉(zhuǎn)載自后端技術(shù)漫談,原文鏈接 https://mp.weixin.qq.com/s/-0_ReIv2bp5snq3NUI3P7A年缎,文章內(nèi)容有部分刪減。
當(dāng)我們在做數(shù)據(jù)庫與緩存數(shù)據(jù)同步時惭墓,究竟更新緩存涡贱,還是刪除緩存,究竟是先操作數(shù)據(jù)庫秦士,還是先操作緩存频鉴?本文帶大家深度分析數(shù)據(jù)庫與緩存的雙寫問題栓辜,并且給出了所有方案的實現(xiàn)代碼方便大家參考。
不更新緩存垛孔,而是刪除緩存
大部分觀點認(rèn)為藕甩,做緩存不應(yīng)該是去更新緩存,而是應(yīng)該刪除緩存似炎,然后由下個請求去去緩存辛萍,發(fā)現(xiàn)不存在后再讀取數(shù)據(jù)庫,寫入緩存羡藐。
觀點引用:《分布式之?dāng)?shù)據(jù)庫和緩存雙寫一致性方案解析》孤獨煙
原因一:線程安全角度
同時有請求A和請求B進行更新操作贩毕,那么會出現(xiàn)
(1)線程A更新了數(shù)據(jù)庫
(2)線程B更新了數(shù)據(jù)庫
(3)線程B更新了緩存
(4)線程A更新了緩存
這就出現(xiàn)請求A更新緩存應(yīng)該比請求B更新緩存早才對,但是因為網(wǎng)絡(luò)等原因仆嗦,B卻比A更早更新了緩存辉阶。這就導(dǎo)致了臟數(shù)據(jù),因此不考慮瘩扼。
原因二:業(yè)務(wù)場景角度
有如下兩點:
(1)如果你是一個寫數(shù)據(jù)庫場景比較多谆甜,而讀數(shù)據(jù)場景比較少的業(yè)務(wù)需求,采用這種方案就會導(dǎo)致集绰,數(shù)據(jù)壓根還沒讀到规辱,緩存就被頻繁的更新,浪費性能栽燕。
(2)如果你寫入數(shù)據(jù)庫的值罕袋,并不是直接寫入緩存的改淑,而是要經(jīng)過一系列復(fù)雜的計算再寫入緩存。那么浴讯,每次寫入數(shù)據(jù)庫后朵夏,都再次計算寫入緩存的值,無疑是浪費性能的榆纽。顯然仰猖,刪除緩存更為適合。
其實如果業(yè)務(wù)非常簡單奈籽,只是去數(shù)據(jù)庫拿一個值饥侵,寫入緩存,那么更新緩存也是可以的唠摹。但是爆捞,淘汰緩存操作簡單奉瘤,并且?guī)淼母弊饔弥皇窃黾恿艘淮蝐ache miss勾拉,建議作為通用的處理方式。
先操作緩存盗温,還是先操作數(shù)據(jù)庫藕赞?
那么問題就來了,我們是先刪除緩存卖局,然后再更新數(shù)據(jù)庫斧蜕,還是先更新數(shù)據(jù)庫,再刪緩存呢砚偶?
先來看看大佬們怎么說批销。《【58沈劍架構(gòu)系列】緩存架構(gòu)設(shè)計細(xì)節(jié)二三事》58沈劍:
對于一個不能保證事務(wù)性的操作染坯,一定涉及“哪個任務(wù)先做均芽,哪個任務(wù)后做”的問題,解決這個問題的方向是:如果出現(xiàn)不一致单鹿,誰先做對業(yè)務(wù)的影響較小掀宋,就誰先執(zhí)行。
假設(shè)先淘汰緩存仲锄,再寫數(shù)據(jù)庫:第一步淘汰緩存成功劲妙,第二步寫數(shù)據(jù)庫失敗,則只會引發(fā)一次Cache miss儒喊。
假設(shè)先寫數(shù)據(jù)庫镣奋,再淘汰緩存:第一步寫數(shù)據(jù)庫操作成功,第二步淘汰緩存失敗怀愧,則會出現(xiàn)DB中是新數(shù)據(jù)侨颈,Cache中是舊數(shù)據(jù)富雅,數(shù)據(jù)不一致。
沈劍老師說的沒有問題肛搬,不過沒完全考慮好并發(fā)請求時的數(shù)據(jù)臟讀問題没佑,讓我們再來看看孤獨煙老師《分布式之?dāng)?shù)據(jù)庫和緩存雙寫一致性方案解析》:
先刪緩存,再更新數(shù)據(jù)庫
該方案會導(dǎo)致請求數(shù)據(jù)不一致温赔。假設(shè)同時有一個請求A進行更新操作蛤奢,另一個請求B進行查詢操作。那么會出現(xiàn)如下情形:
(1)請求A進行寫操作陶贼,刪除緩存
(2)請求B查詢發(fā)現(xiàn)緩存不存在
(3)請求B去數(shù)據(jù)庫查詢得到舊值
(4)請求B將舊值寫入緩存
(5)請求A將新值寫入數(shù)據(jù)庫
上述情況就會導(dǎo)致不一致的情形出現(xiàn)啤贩。而且,如果不采用給緩存設(shè)置過期時間策略拜秧,該數(shù)據(jù)永遠(yuǎn)都是臟數(shù)據(jù)痹屹。
所以先刪緩存,再更新數(shù)據(jù)庫并不是一勞永逸的解決方案枉氮,再看看先更新數(shù)據(jù)庫志衍,再刪緩存這種方案怎么樣?
先更新數(shù)據(jù)庫聊替,再刪緩存這種情況不存在并發(fā)問題么楼肪?
不是的。假設(shè)同時有一個請求A做查詢操作惹悄,一個請求B做更新操作春叫,那么會有如下情形產(chǎn)生
(1)緩存剛好失效
(2)請求A查詢數(shù)據(jù)庫,得一個舊值
(3)請求B將新值寫入數(shù)據(jù)庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
ok泣港,如果發(fā)生上述情況暂殖,確實是會發(fā)生臟數(shù)據(jù)。
然而当纱,發(fā)生這種情況的概率又有多少呢呛每?
發(fā)生上述情況有一個先天性條件,就是步驟(3)的寫數(shù)據(jù)庫操作比步驟(2)的讀數(shù)據(jù)庫操作耗時更短惫东,才有可能使得步驟(4)先于步驟(5)莉给。可是廉沮,大家想想颓遏,數(shù)據(jù)庫的讀操作的速度遠(yuǎn)快于寫操作的(不然做讀寫分離干嘛,做讀寫分離的意義就是因為讀操作比較快滞时,耗資源少)叁幢,因此步驟(3)耗時比步驟(2)更短,這一情形很難出現(xiàn)坪稽。
先更新數(shù)據(jù)庫曼玩,再刪緩存依然會有問題鳞骤,不過,問題出現(xiàn)的可能性會因為上面說的原因黍判,變得比較低豫尽!
所以,如果你想實現(xiàn)基礎(chǔ)的緩存數(shù)據(jù)庫雙寫一致的邏輯顷帖,那么在大多數(shù)情況下美旧,在不想做過多設(shè)計,增加太大工作量的情況下贬墩,請先更新數(shù)據(jù)庫榴嗅,再刪緩存!
非要保證數(shù)據(jù)庫和緩存數(shù)據(jù)強一致性該怎么辦?
那么陶舞,如果我非要保證絕對一致性怎么辦嗽测,先給出結(jié)論:
沒有辦法做到絕對的一致性,這是由CAP理論決定的肿孵,緩存系統(tǒng)適用的場景就是非強一致性的場景唠粥,所以它屬于CAP中的AP。
CAP 定理又被稱作布魯爾定理颁井,厅贪,它指出對于一個分布式計算系統(tǒng)來說,不可能同時滿足以下三點:
- 一致性(Consistency)(等同于所有節(jié)點訪問同一份最新的數(shù)據(jù)副本)
- 可用性(Availability)(每次請求都能獲取到非錯的相應(yīng)——但是不保證獲取的數(shù)據(jù)為最新數(shù)據(jù))
- 分區(qū)容錯性(Partition tolerance)(以實際效果而言雅宾,分區(qū)相當(dāng)于對通信的時限要求。系統(tǒng)如果不能在時限內(nèi)達成數(shù)據(jù)一致性葵硕,就意味著發(fā)生了分區(qū)的情況眉抬,必須就當(dāng)前操作在C和A之間做出選擇。)
所以懈凹,我們得委曲求全蜀变,可以去做到BASE理論中說的最終一致性。
最終一致性強調(diào)的是系統(tǒng)中所有的數(shù)據(jù)副本介评,在經(jīng)過一段時間的同步后库北,最終能夠達到一個一致的狀態(tài)。因此们陆,最終一致性的本質(zhì)是需要系統(tǒng)保證最終數(shù)據(jù)能夠達到一致寒瓦,而不需要實時保證系統(tǒng)數(shù)據(jù)的強一致性
大佬們給出了到達最終一致性的解決思路,主要是針對上面兩種雙寫策略(先刪緩存坪仇,再更新數(shù)據(jù)庫/先更新數(shù)據(jù)庫杂腰,再刪緩存)導(dǎo)致的臟數(shù)據(jù)問題,進行相應(yīng)的處理椅文,來保證最終一致性喂很。
緩存延時雙刪
問:先刪除緩存惜颇,再更新數(shù)據(jù)庫中如何避免臟數(shù)據(jù)?
答:采用延時雙刪策略少辣。
上文我們提到凌摄,在先刪除緩存,再更新數(shù)據(jù)庫的情況下漓帅,如果不給緩存設(shè)置過期時間望伦,那么該數(shù)據(jù)永遠(yuǎn)都是臟數(shù)據(jù)。
那么延時雙刪怎么解決這個問題呢煎殷?
(1)先淘汰緩存
(2)再寫數(shù)據(jù)庫(這兩步和原來一樣)
(3)休眠1秒屯伞,再次淘汰緩存
這么做,可以將1秒內(nèi)所造成的緩存臟數(shù)據(jù)豪直,再次刪除劣摇。
那么,這個1秒怎么確定的弓乙,具體該休眠多久呢末融?
針對上面的情形,讀者應(yīng)該自行評估自己的項目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時暇韧。然后寫數(shù)據(jù)的休眠時間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時基礎(chǔ)上勾习,加幾百ms即可。這么做的目的懈玻,就是確保讀請求結(jié)束巧婶,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。
如果你用了mysql的讀寫分離架構(gòu)怎么辦涂乌?
ok艺栈,在這種情況下,造成數(shù)據(jù)不一致的原因如下湾盒,還是兩個請求湿右,一個請求A進行更新操作,另一個請求B進行查詢操作罚勾。
(1)請求A進行寫操作毅人,刪除緩存
(2)請求A將數(shù)據(jù)寫入數(shù)據(jù)庫了,
(3)請求B查詢緩存發(fā)現(xiàn)尖殃,緩存沒有值
(4)請求B去從庫查詢丈莺,這時,還沒有完成主從同步分衫,因此查詢到的是舊值
(5)請求B將舊值寫入緩存
(6)數(shù)據(jù)庫完成主從同步场刑,從庫變?yōu)樾轮?/p>
上述情形,就是數(shù)據(jù)不一致的原因。還是使用雙刪延時策略牵现。只是铐懊,睡眠時間修改為在主從同步的延時時間基礎(chǔ)上,加幾百ms瞎疼。
采用這種同步淘汰策略科乎,吞吐量降低怎么辦?
ok贼急,那就將第二次刪除作為異步的茅茂。自己起一個線程,異步刪除太抓。這樣空闲,寫的請求就不用沉睡一段時間后了,再返回走敌。這么做碴倾,加大吞吐量。
所以在先刪除緩存掉丽,再更新數(shù)據(jù)庫的情況下跌榔,可以使用延時雙刪的策略,來保證臟數(shù)據(jù)只會存活一段時間捶障,就會被準(zhǔn)確的數(shù)據(jù)覆蓋僧须。
在先更新數(shù)據(jù)庫,再刪緩存的情況下项炼,緩存出現(xiàn)臟數(shù)據(jù)的情況雖然可能性極小担平,但也會出現(xiàn)。我們依然可以用延時雙刪策略芥挣,在請求A對緩存寫入了臟的舊值之后驱闷,再次刪除緩存。來保證去掉臟緩存空免。
刪緩存失敗了怎么辦:重試機制
看似問題都已經(jīng)解決了,但其實盆耽,還有一個問題沒有考慮到蹋砚,那就是刪除緩存的操作,失敗了怎么辦摄杂?比如延時雙刪的時候坝咐,第二次緩存刪除失敗了,那不還是沒有清除臟數(shù)據(jù)嗎析恢?
解決方案就是再加上一個重試機制墨坚,保證刪除緩存成功。
參考孤獨煙老師給的方案圖:
方案一:
流程如下所示
(1)更新數(shù)據(jù)庫數(shù)據(jù)映挂;
(2)緩存因為種種問題刪除失敗
(3)將需要刪除的key發(fā)送至消息隊列
(4)自己消費消息泽篮,獲得需要刪除的key
(5)繼續(xù)重試刪除操作盗尸,直到成功
然而,該方案有一個缺點帽撑,對業(yè)務(wù)線代碼造成大量的侵入泼各。于是有了方案二,在方案二中亏拉,啟動一個訂閱程序去訂閱數(shù)據(jù)庫的binlog扣蜻,獲得需要操作的數(shù)據(jù)。在應(yīng)用程序中及塘,另起一段程序莽使,獲得這個訂閱程序傳來的信息,進行刪除緩存操作笙僚。
方案二:
流程如下圖所示:
(1)更新數(shù)據(jù)庫數(shù)據(jù)
(2)數(shù)據(jù)庫會將操作信息寫入binlog日志當(dāng)中
(3)訂閱程序提取出所需要的數(shù)據(jù)以及key
(4)另起一段非業(yè)務(wù)代碼芳肌,獲得該信息
(5)嘗試刪除緩存操作,發(fā)現(xiàn)刪除失敗
(6)將這些信息發(fā)送至消息隊列
(7)重新從消息隊列中獲得該數(shù)據(jù)味咳,重試操作庇勃。
這里讀取binlog的中間件,可以采用阿里開源的canal槽驶。
好了责嚷,到這里我們已經(jīng)把緩存雙寫一致性的思路徹底梳理了一遍,下面就是我對這幾種思路徒手寫的實戰(zhàn)代碼掂铐,方便有需要的朋友參考罕拂。
小結(jié)
引用陳浩《緩存更新的套路》最后的總結(jié)語作為小結(jié):
分布式系統(tǒng)里要么通過2PC或是Paxos協(xié)議保證一致性,要么就是拼命的降低并發(fā)時臟數(shù)據(jù)的概率
緩存系統(tǒng)適用的場景就是非強一致性的場景全陨,所以它屬于CAP中的AP爆班,BASE理論。
異構(gòu)數(shù)據(jù)庫本來就沒辦法強一致辱姨,只是盡可能減少時間窗口柿菩,達到最終一致性。
還有別忘了設(shè)置過期時間雨涛,這是個兜底方案枢舶。
文章內(nèi)容大致可以總結(jié)為如下幾點:
- 對于讀多寫少的數(shù)據(jù),請使用緩存替久。
- 為了保持?jǐn)?shù)據(jù)庫和緩存的一致性凉泄,會導(dǎo)致系統(tǒng)吞吐量的下降。
- 為了保持?jǐn)?shù)據(jù)庫和緩存的一致性蚯根,會導(dǎo)致業(yè)務(wù)代碼邏輯復(fù)雜后众。
- 緩存做不到絕對一致性,但可以做到最終一致性。
- 對于需要保證緩存數(shù)據(jù)庫數(shù)據(jù)一致的情況蒂誉,請盡量考慮對一致性到底有多高要求教藻,選定合適的方案,避免過度設(shè)計拗盒。