再有人問(wèn)你數(shù)據(jù)庫(kù)緩存一致性的問(wèn)題,直接把這篇文章發(fā)給他!


我們提到過(guò),在數(shù)據(jù)庫(kù)和緩存的操作過(guò)程中,可能存在”先寫(xiě)數(shù)據(jù)庫(kù),后刪緩存”、”先寫(xiě)數(shù)據(jù)庫(kù),后更新緩存”、”先刪緩存庫(kù),后寫(xiě)數(shù)據(jù)庫(kù)”以及”先更新緩存庫(kù),后寫(xiě)數(shù)據(jù)庫(kù)”這四種。
那么,到底是應(yīng)該刪除緩存好呢,還是更新緩存好呢?到底應(yīng)該先操作數(shù)據(jù)庫(kù)呢還是先操作緩存呢?哪種方案更好呢?又該如何選擇呢?
本文就來(lái)展開(kāi)分析一下。
為了保證數(shù)據(jù)庫(kù)和緩存里面的數(shù)據(jù)是一致的,很多人會(huì)很多人在做數(shù)據(jù)更新的時(shí)候,會(huì)同時(shí)更新緩存里面的內(nèi)容。但是我其實(shí)告訴大家,應(yīng)該優(yōu)先選擇刪除緩存而不是更新緩存。
首先,我們暫時(shí)拋開(kāi)數(shù)據(jù)一致性的問(wèn)題,單獨(dú)來(lái)看看更新緩存和刪除緩存的復(fù)雜的的問(wèn)題。
我們放到緩存中的數(shù)據(jù),很多時(shí)候可能不只是簡(jiǎn)單的一個(gè)字符串類(lèi)型的值,他還可能是一個(gè)大的JSON串,一個(gè)map類(lèi)型等等。
舉個(gè)栗子,我們需要通過(guò)緩存進(jìn)行扣減庫(kù)存的時(shí)候,你可能需要從緩存中查出整個(gè)訂單模型數(shù)據(jù),把他進(jìn)行反序列化之后,再解析出其中的庫(kù)存字段,把他修改掉,然后再序列化,最后再更新到緩存中。
可以看到,更新緩存的動(dòng)作,相比于直接刪除緩存,操作過(guò)程比較的復(fù)雜,而且也容易出錯(cuò)。
還有就是,在數(shù)據(jù)庫(kù)和緩存的一致性保證方面,刪除緩存相比更新緩存要更簡(jiǎn)單一點(diǎn)。
我們?cè)凇?/span>為什么會(huì)出現(xiàn)數(shù)據(jù)庫(kù)和緩存不一致的問(wèn)題》中介紹過(guò)的"寫(xiě)寫(xiě)并發(fā)"的場(chǎng)景中,如果同時(shí)更新緩存和數(shù)據(jù)庫(kù),那么很容易會(huì)出現(xiàn)因?yàn)椴l(fā)的問(wèn)題導(dǎo)致數(shù)據(jù)不一致的情況。如:
先寫(xiě)數(shù)據(jù)庫(kù),再更新緩存

先更新緩存,后寫(xiě)數(shù)據(jù)庫(kù):

但是,如果是做緩存的刪除的話(huà),在寫(xiě)寫(xiě)并發(fā)的情況下,緩存中的數(shù)據(jù)都是要被清除的,所以就不會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。
但是,更新緩存相比刪除緩存還是有一個(gè)小的缺點(diǎn),那就是帶來(lái)的一次額外的cache miss,也就是說(shuō)在刪除緩存后的下一次查詢(xún)會(huì)無(wú)法命中緩存,要查詢(xún)一下數(shù)據(jù)庫(kù)。
這種cache miss在某種程度上可能會(huì)導(dǎo)致緩存擊穿,也就是剛好緩存被刪除之后,同一個(gè)Key有大量的請(qǐng)求過(guò)來(lái),導(dǎo)致緩存被擊穿,大量請(qǐng)求訪(fǎng)問(wèn)到數(shù)據(jù)庫(kù)。
但是,通過(guò)加鎖的方式是可以比較方便的解決緩存擊穿的問(wèn)題的。
總之,刪除緩存相比較更新緩存,方案更加簡(jiǎn)單,而且?guī)?lái)的一致性問(wèn)題也更少。所以,在刪除和更新緩存之間,我還是偏向于建議大家優(yōu)先選擇刪除緩存。
在確定了優(yōu)先選擇刪除緩存而不是更新緩存之后,留給我們的數(shù)據(jù)庫(kù)+緩存更新的可選方案就剩下:"先寫(xiě)數(shù)據(jù)庫(kù)后刪除緩存"和"先刪除緩存后寫(xiě)數(shù)據(jù)庫(kù)了"。
那么,這兩種方式各自有什么優(yōu)缺點(diǎn)呢?該如何選擇呢?
而一般情況下,如果把緩存的刪除動(dòng)作放到第二步,有一個(gè)好處,那就是緩存刪除失敗的概率還是比較低的,除非是網(wǎng)絡(luò)問(wèn)題或者緩存服務(wù)器宕機(jī)的問(wèn)題,否則大部分情況都是可以成功的。
還有就是,先寫(xiě)數(shù)據(jù)庫(kù)后刪除緩存雖然不存在"寫(xiě)寫(xiě)并發(fā)"導(dǎo)致的數(shù)據(jù)一致性問(wèn)題,但是會(huì)存在"讀寫(xiě)并發(fā)"情況下的數(shù)據(jù)一致性問(wèn)題。
我們知道,當(dāng)我們使用了緩存之后,一個(gè)讀的線(xiàn)程在查詢(xún)數(shù)據(jù)的過(guò)程是這樣的:
1、查詢(xún)緩存,如果緩存中有值,則直接返回
2、查詢(xún)數(shù)據(jù)庫(kù)
3、把數(shù)據(jù)庫(kù)的查詢(xún)結(jié)果更新到緩存中
所以,對(duì)于一個(gè)讀線(xiàn)程來(lái)說(shuō),雖然不會(huì)寫(xiě)數(shù)據(jù)庫(kù),但是是會(huì)更新緩存的,所以,在一些特殊的并發(fā)場(chǎng)景中,就會(huì)導(dǎo)致數(shù)據(jù)不一致的情況。
讀寫(xiě)并發(fā)的時(shí)序如下:

也就是說(shuō),假如一個(gè)讀線(xiàn)程,在讀緩存的時(shí)候沒(méi)查到值,他就會(huì)去數(shù)據(jù)庫(kù)中查詢(xún),但是如果自查詢(xún)到結(jié)果之后,更新緩存之前,數(shù)據(jù)庫(kù)被更新了,但是這個(gè)讀線(xiàn)程是完全不知道的,那么就導(dǎo)致最終緩存會(huì)被重新用一個(gè)"舊值"覆蓋掉。
這也就導(dǎo)致了緩存和數(shù)據(jù)庫(kù)的不一致的現(xiàn)象。
但是這種現(xiàn)象其實(shí)發(fā)生的概率比較低,因?yàn)橐话阋粋€(gè)讀操作是很快的,數(shù)據(jù)庫(kù)+緩存的讀操作基本在十幾毫秒左右就可以完成了。
而在這期間,更好另一個(gè)線(xiàn)程執(zhí)行了一個(gè)比較耗時(shí)的寫(xiě)操作的概率確實(shí)比較低。
先刪緩存
那么,如果是先刪除緩存后操作數(shù)據(jù)庫(kù)的話(huà),會(huì)不會(huì)方案更完美一點(diǎn)呢?
首先,如果是選擇先刪除緩存后寫(xiě)數(shù)據(jù)庫(kù)的這種方案,那么第二步的失敗是可以接受的,因?yàn)檫@樣不會(huì)有臟數(shù)據(jù),也沒(méi)什么影響,只需要重試就好了。
但是,先刪除緩存后寫(xiě)數(shù)據(jù)庫(kù)的這種方式,會(huì)無(wú)形中放大前面我們提到的"讀寫(xiě)并發(fā)"導(dǎo)致的數(shù)據(jù)不一致的問(wèn)題。
因?yàn)檫@種"讀寫(xiě)并發(fā)"問(wèn)題發(fā)生的前提是讀線(xiàn)程讀緩存沒(méi)讀到值,而先刪緩存的動(dòng)作一旦發(fā)生,剛好可以讓讀線(xiàn)程就從緩存中讀不到值。
所以,本來(lái)一個(gè)小概率會(huì)發(fā)生的"讀寫(xiě)并發(fā)"問(wèn)題,在先刪緩存的過(guò)程中,問(wèn)題發(fā)生的概率會(huì)被放大。
而且這種問(wèn)題的后果也比較嚴(yán)重,那就是緩存中的值一直是錯(cuò)的,就會(huì)導(dǎo)致后續(xù)的所以命中緩存的查詢(xún)結(jié)果都是錯(cuò)的!
那么,雖然先寫(xiě)數(shù)據(jù)后刪除緩存的這種情況,可以大大的降低并發(fā)問(wèn)題的概率,但是,根據(jù)墨菲定律,只要有可能發(fā)生的壞事,那就基本上會(huì)發(fā)生。越是龐大的系統(tǒng)發(fā)生的概率越高。
那么,有沒(méi)有什么辦法可以來(lái)解決一下這種情況帶來(lái)的不一致的問(wèn)題呢?
其實(shí)是有一個(gè)比較常見(jiàn)的方案的,在很多公司內(nèi)用的也比較多,那就是延遲雙刪。
因?yàn)?讀寫(xiě)并發(fā)"的問(wèn)題會(huì)導(dǎo)致并發(fā)發(fā)生后,緩存中的數(shù)被讀線(xiàn)程寫(xiě)進(jìn)去臟數(shù)據(jù),那么就只需要在寫(xiě)線(xiàn)程在寫(xiě)數(shù)據(jù)庫(kù)、刪緩存之后,延遲一段時(shí)間,在執(zhí)行一把刪除動(dòng)作就行了。
這樣就能保證緩存中的臟數(shù)據(jù)被清理掉,避免后續(xù)的讀操作都讀到臟數(shù)據(jù)。當(dāng)然,這個(gè)延遲的時(shí)長(zhǎng)也很講究,到底多久來(lái)刪除呢?一般建議設(shè)置1-2s就可以了。
當(dāng)然,這種方案也是有一個(gè)弊端的,那就是可能會(huì)導(dǎo)致緩存中準(zhǔn)確的數(shù)據(jù)被刪除掉。當(dāng)然這也問(wèn)題不大,就像我們前面說(shuō)過(guò)的,只是增加一次cache miss罷了
前面介紹了幾種情況的具體問(wèn)題和解決方案,那么實(shí)際工作中應(yīng)該如何選擇呢?
我覺(jué)得主要還是根據(jù)實(shí)際的業(yè)務(wù)情況來(lái)分析。
比如,如果業(yè)務(wù)量不大,并發(fā)不高的情況,可以選擇先刪除緩存,后更新數(shù)據(jù)庫(kù)的方式,因?yàn)檫@種方案更加簡(jiǎn)單。
但是,如果是業(yè)務(wù)量比較大,并發(fā)度很高的話(huà),那么建議選擇先更新數(shù)據(jù)庫(kù),后刪除緩存的方式,因?yàn)檫@種方式并發(fā)問(wèn)題更少一些。但是可能會(huì)引入加鎖、延遲雙刪等更多機(jī)制,使得整個(gè)方案會(huì)更加復(fù)雜。
其實(shí),先操作數(shù)據(jù)庫(kù),后操作緩存,是一種比較典型的設(shè)計(jì)模式——Cache Aside Pattern。
這種模式的主要方案就是先寫(xiě)數(shù)據(jù)庫(kù),后刪緩存,而且緩存的刪除是可以在旁路異步執(zhí)行的。
這種模式的優(yōu)點(diǎn)就是我們說(shuō)的,他可以解決"寫(xiě)寫(xiě)并發(fā)"導(dǎo)致的數(shù)據(jù)不一致問(wèn)題,并且可以大大降低"讀寫(xiě)并發(fā)"的問(wèn)題,所以這也是Facebook比較推崇的一種模式。
Cache Aside Pattern 這種模式中,我們可以異步的在旁路處理緩存。其實(shí)這種方案在大廠中確實(shí)有的還蠻多的。
主要的方式就是借助數(shù)據(jù)庫(kù)的binlog或者基于異步消息訂閱的方式。
也就是說(shuō),在代碼的主要邏輯中,先操作數(shù)據(jù)庫(kù)就行了,然后數(shù)據(jù)庫(kù)操作完,可以發(fā)一個(gè)異步消息出來(lái)。
然后再由一個(gè)監(jiān)聽(tīng)者在接到消息之后,異步的把緩存中的數(shù)據(jù)刪除掉。
或者干脆借助數(shù)據(jù)庫(kù)的binlog,訂閱到數(shù)據(jù)庫(kù)變更之后,異步的清除緩存。
這兩種方式都會(huì)有一定的延時(shí),通常在毫秒級(jí)別,一般用于在可接受秒級(jí)延遲的業(yè)務(wù)場(chǎng)景中。
前面介紹過(guò)了Cache Aside Pattern這種關(guān)于緩存操作的設(shè)計(jì)模式,那么其實(shí)還有幾種其他的設(shè)計(jì)模式,也一起展開(kāi)介紹一下:
Read/Write Through Pattern
在這兩種模式中,應(yīng)用程序?qū)⒕彺孀鳛橹饕臄?shù)據(jù)源,不需要感知數(shù)據(jù)庫(kù),更新數(shù)據(jù)庫(kù)和從數(shù)據(jù)庫(kù)的讀取的任務(wù)都交給緩存來(lái)代理。
Read Through模式下,是由緩存配置一個(gè)讀模塊,它知道如何將數(shù)據(jù)庫(kù)中的數(shù)據(jù)寫(xiě)入緩存。在數(shù)據(jù)被請(qǐng)求的時(shí)候,如果未命中,則將數(shù)據(jù)從數(shù)據(jù)庫(kù)載入緩存。
Write Through模式下,緩存配置一個(gè)寫(xiě)模塊,它知道如何將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)。當(dāng)應(yīng)用要寫(xiě)入數(shù)據(jù)時(shí),緩存會(huì)先存儲(chǔ)數(shù)據(jù),并調(diào)用寫(xiě)模塊將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)。
也就是說(shuō),這兩種模式下,不需要應(yīng)用自己去操作數(shù)據(jù)庫(kù),緩存自己就把活干完了。
Write Behind Caching Pattern
這種模式就是在更新數(shù)據(jù)的時(shí)候,只更新緩存,而不更新數(shù)據(jù)庫(kù),然后再異步的定時(shí)把緩存中的數(shù)據(jù)持久化到數(shù)據(jù)庫(kù)中。
這種模式的優(yōu)缺點(diǎn)比較明顯,那就是讀寫(xiě)速度都很快,但是會(huì)造成一定的數(shù)據(jù)丟失。
這種比較適合用在比如統(tǒng)計(jì)文章的訪(fǎng)問(wèn)量、點(diǎn)贊等場(chǎng)景中,允許數(shù)據(jù)少量丟失,但是速度要快。
《人月神話(huà)》的作者Fred Brooks在早年有一篇很著名文章《No Silver Bullet》 ,他提到:
在軟件開(kāi)發(fā)過(guò)程里是沒(méi)有萬(wàn)能的終殺性武器的,只有各種方法綜合運(yùn)用,才是解決之道。而各種聲稱(chēng)如何如何神奇的理論或方法,都不是能殺死“軟件危機(jī)”這頭人狼的銀彈。
也就是說(shuō),沒(méi)有哪種技術(shù)手段或者方案,是放之四海皆準(zhǔn)的。如果有的話(huà),我們這些工程師也就沒(méi)有存在的必要了。
所以,任何的技術(shù)方案,都是一個(gè)權(quán)衡的過(guò)程,要權(quán)衡的問(wèn)題有很多,業(yè)務(wù)的具體情況,實(shí)現(xiàn)的復(fù)雜度、實(shí)現(xiàn)的成本,團(tuán)隊(duì)成員的接受度、可維護(hù)性、容易理解的程度等等。
所以,沒(méi)有一個(gè)"完美"的方案,只有"適合"的方案。
但是,如何能選出一個(gè)適合的方案,這里面就需要有很多的輸入來(lái)做支撐了。希望本文的內(nèi)容可以為你日后的決策提供一點(diǎn)參考!
歡迎添加小編微信,進(jìn)入交流群
推薦閱讀:
