關(guān)于如何更新緩存的探討

寫這篇文章的原因

現(xiàn)在我們的系統(tǒng)都需要使用緩存提高性能,使用緩存就需要對(duì)緩存進(jìn)行維護(hù),那么當(dāng)數(shù)據(jù)發(fā)生變化時(shí)我們應(yīng)該先操作緩存還是先操作數(shù)據(jù)庫(kù)呢?網(wǎng)上有兩篇很好的文章,一篇是來自58沈劍的架構(gòu)師之路系列之緩存架構(gòu)設(shè)計(jì)緩存架構(gòu)設(shè)計(jì),一篇來自于左耳朵耗子陳皓的緩存更新的套路,兩位老師給出了很好的分析,這里分別總結(jié)一下,希望能對(duì)看過的同學(xué)有所幫助。

架構(gòu)師之路

先來說一下沈劍老師的文章。

當(dāng)數(shù)據(jù)庫(kù)執(zhí)行更新操作時(shí),我們會(huì)進(jìn)行緩存的淘汰,由于操作緩存與操作數(shù)據(jù)庫(kù)并不能保證原子性,所以解題思路就是:如果出現(xiàn)不一致,誰先做對(duì)業(yè)務(wù)的影響較小,就誰先執(zhí)行。分別分析下

先寫數(shù)據(jù)庫(kù)的情況:第一步寫數(shù)據(jù)庫(kù)操作成功,第二步淘汰緩存失敗,則會(huì)出現(xiàn)DB中是新數(shù)據(jù),Cache中是舊數(shù)據(jù),數(shù)據(jù)不一致。

先淘汰緩存的情況:第一步淘汰緩存成功,第二步寫數(shù)據(jù)庫(kù)失敗,則只會(huì)引發(fā)一次Cache miss。

所以結(jié)論是:先淘汰緩存,再寫數(shù)據(jù)庫(kù)

陳皓-酷殼

剛開始看的時(shí)候讓我驚訝的是,陳皓老師文章開篇就指出了先淘汰緩存再更新數(shù)據(jù)庫(kù)的做法是錯(cuò)誤的。

給出的理由如下:兩個(gè)并發(fā)操作,一個(gè)是更新操作,另一個(gè)是查詢操作,更新操作刪除緩存后,查詢操作沒有命中緩存,先把老數(shù)據(jù)讀出來后放到緩存中,然后更新操作更新了數(shù)據(jù)庫(kù)。于是,在緩存中的數(shù)據(jù)還是老的數(shù)據(jù),導(dǎo)致緩存中的數(shù)據(jù)是臟的,而且還一直這樣臟下去了(好像沒毛病)

接下來列舉了幾個(gè)常用的緩存模式

首先是Cache aside

以下是對(duì)Cache aside幾種緩存狀態(tài)的處理:

失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中。

命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。

更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后,再讓緩存失效。

這個(gè)更新操作就不會(huì)發(fā)生陳皓老師開篇時(shí)提到的問題。舉個(gè)??,一個(gè)查詢操作和一個(gè)更新操作的并發(fā),首先,沒有了刪除cache數(shù)據(jù)的操作了,而是先更新了數(shù)據(jù)庫(kù)中的數(shù)據(jù),此時(shí),緩存依然有效,所以,并發(fā)的查詢操作拿的是沒有更新的數(shù)據(jù),但是,更新操作馬上讓緩存的失效了,后續(xù)的查詢操作再把數(shù)據(jù)從數(shù)據(jù)庫(kù)中拉出來。這樣后續(xù)的查詢操作就會(huì)拉取最新的數(shù)據(jù)。

并且陳皓老師也指出,F(xiàn)acebook的論文《Scaling Memcache at Facebook》也使用了這個(gè)策略。這樣做的目的主要是避免兩個(gè)并發(fā)的寫操作導(dǎo)致臟數(shù)據(jù)。

但是隨后又指出這個(gè)模式也會(huì)出現(xiàn)不一致的情況,舉個(gè)??,一個(gè)是讀操作,但是沒有命中緩存,然后就到數(shù)據(jù)庫(kù)中取數(shù)據(jù),此時(shí)來了一個(gè)寫操作,寫完數(shù)據(jù)庫(kù)后,讓緩存失效,然后,之前的那個(gè)讀操作再把老的數(shù)據(jù)放進(jìn)去,所以,會(huì)造成臟數(shù)據(jù)。這個(gè)case理論上會(huì)出現(xiàn),不過出現(xiàn)的概率可能非常低,因?yàn)檫@個(gè)條件需要發(fā)生在讀緩存時(shí)緩存失效,而且并發(fā)著有一個(gè)寫操作。而實(shí)際上數(shù)據(jù)庫(kù)的寫操作會(huì)比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進(jìn)入數(shù)據(jù)庫(kù)操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的概率基本并不大。

所以使用先操作數(shù)據(jù)庫(kù)后操作緩存的方法會(huì)大大降低并發(fā)時(shí)臟數(shù)據(jù)的概率,并且為了盡量避免上文的低概率事件,最好為緩存設(shè)置過期時(shí)間。

這里陳皓老師得出了與沈劍老師相反的結(jié)論,陳皓老師在文章的末尾給出了答案“上面,我們沒有考慮緩存(Cache)和持久層(Repository)的整體事務(wù)的問題”,假設(shè)原子性得以保障(可以使用2PC,3PC,Paxos等算法進(jìn)行保障),那么先操作數(shù)據(jù)庫(kù)則是最優(yōu)的選擇。兩位老師的結(jié)論先放一邊,繼續(xù)。

陳皓老師又給我們開了點(diǎn)小灶,介紹了其他常用的緩存模式。

Read/Write Through Pattern

Read Through 套路就是在查詢操作中更新緩存,也就是說,當(dāng)緩存失效的時(shí)候(過期或LRU換出),Cache Aside是由調(diào)用方負(fù)責(zé)把數(shù)據(jù)加載入緩存,而Read Through則用緩存服務(wù)自己來加載,從而對(duì)應(yīng)用方是透明的。

Write Through 套路和Read Through相仿,不過是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒有命中緩存,直接更新數(shù)據(jù)庫(kù),然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己更新數(shù)據(jù)庫(kù)(這是一個(gè)同步操作)

Write Behind Caching Pattern

Write Behind 又叫 Write Back。Write Back一句說就是,在更新數(shù)據(jù)的時(shí)候,只更新緩存,不更新數(shù)據(jù)庫(kù),而我們的緩存會(huì)異步地批量更新數(shù)據(jù)庫(kù)。這個(gè)設(shè)計(jì)的好處就是讓數(shù)據(jù)的I/O操作飛快無比,因?yàn)楫惒?,write backg還可以合并對(duì)同一個(gè)數(shù)據(jù)的多次操作,所以性能的提高是相當(dāng)可觀的。

但是,其帶來的問題是,數(shù)據(jù)不是強(qiáng)一致性的,而且可能會(huì)丟失(我們知道Unix/Linux非正常關(guān)機(jī)會(huì)導(dǎo)致數(shù)據(jù)丟失,就是因?yàn)檫@個(gè)事)。另外,Write Back實(shí)現(xiàn)邏輯比較復(fù)雜,因?yàn)樗枰猼rack有哪數(shù)據(jù)是被更新了的,需要刷到持久層上。操作系統(tǒng)的write back會(huì)在僅當(dāng)這個(gè)cache需要失效的時(shí)候,才會(huì)被真正持久起來,比如,內(nèi)存不夠了,或是進(jìn)程退出了等情況,這又叫l(wèi)azy write。

轉(zhuǎn)折

本來看到這里我本以為沈劍老師沒有考慮到并發(fā)讀寫的問題,導(dǎo)致文章出了紕漏,直到我看到了他的第二篇文章數(shù)據(jù)與緩存一致性優(yōu)化。

文章開頭給出了讀寫并發(fā)時(shí)導(dǎo)致數(shù)據(jù)不一致的case,同陳皓老師舉的??一樣,就不多說了。不過文章后半部分對(duì)于先操作緩存,后操作數(shù)據(jù)庫(kù)的做法給出了優(yōu)化。

讓同一個(gè)數(shù)據(jù)的訪問能串行化

在一個(gè)服務(wù)中如何做到“讓同一個(gè)數(shù)據(jù)的訪問串行化”,只需要“讓同一個(gè)數(shù)據(jù)的訪問通過同一條DB連接執(zhí)行”就行。如何做到“讓同一個(gè)數(shù)據(jù)的訪問通過同一條DB連接執(zhí)行”,只需要“在DB連接池層面稍微修改,按數(shù)據(jù)取連接即可”。將從連接池獲取數(shù)據(jù)庫(kù)連接的操作修改為CPool.GetDBConnection(longid)【返回id取模相關(guān)聯(lián)的DB連接】。

當(dāng)有多份服務(wù)時(shí),方案同上,想辦法讓對(duì)同一數(shù)據(jù)的訪問落在同一服務(wù)上即可。同樣CPool.GetServiceConnection(longid)【返回id取模相關(guān)聯(lián)的Service連接】。

總結(jié)一下:

(1)修改服務(wù)Service連接池,id取模選取服務(wù)連接,能夠保證同一個(gè)數(shù)據(jù)的讀寫都落在同一個(gè)后端服務(wù)上

(2)修改數(shù)據(jù)庫(kù)DB連接池,id取模選取DB連接,能夠保證同一個(gè)數(shù)據(jù)的讀寫在數(shù)據(jù)庫(kù)層面是串行的

自己的一些思考

總結(jié)完了兩位老師的文章,最后是自己的一些感悟與思考。

因?yàn)槲疑蠈W(xué)的時(shí)候就已經(jīng)看過沈劍老師的第一篇文章,當(dāng)時(shí)看完有種豁然開朗,吊吊吊的感覺,從那以后就一直把先操作緩存后更新數(shù)據(jù)的做法當(dāng)做了最標(biāo)準(zhǔn)的做法(實(shí)際上工作之后發(fā)現(xiàn)項(xiàng)目里也基本都是這樣做的)。直到有一天看到了酷殼上陳皓老師的文章,和我認(rèn)為的“標(biāo)準(zhǔn)做法”完全相反啊,這是怎么回事?后來經(jīng)過對(duì)文章的仔細(xì)閱讀才理清楚,看到了沈劍老師的第二篇文章也才明白第一篇文章只是個(gè)上集,原來還有下集。總結(jié)一點(diǎn),學(xué)知識(shí)不能快餐文化,也不能“逆來順受”,更不能“淺嘗輒止”,我們需要有自己的思考,需要自己的總結(jié)。(寫博客就是挺好的一種總結(jié)方式)

最后關(guān)于緩存更新倆種方案該選擇哪一種,我認(rèn)為,如果系統(tǒng)并發(fā)量較小,那么選擇先淘汰緩存的做法(不做后續(xù)連接取模等操作)是比較好的。如果并發(fā)量較大,并且緩存系統(tǒng)做了集群,網(wǎng)絡(luò)極少發(fā)生抖動(dòng)(也就是極大程度可以保證原子性),那么選擇先操作數(shù)據(jù)庫(kù)后操作緩存的做法較好。而關(guān)于做連接取模與使用2PC等方案保證數(shù)據(jù)一致性,個(gè)人感覺沒有必要,徒增復(fù)雜性,因?yàn)樯婕皫?kù)存等重要的數(shù)據(jù)操作無論如何最后都要查詢真實(shí)的DB,給緩存數(shù)據(jù)設(shè)置過期時(shí)間減少不一致發(fā)生的概率與存在時(shí)間即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容