引言
數(shù)據(jù)庫跟緩存,或者用Mysql和Redis來代替,想必每個(gè)CRUD boy都不會(huì)陌生。本文要聊的也是一個(gè)經(jīng)典問題,就是以怎樣的方式去操作數(shù)據(jù)庫和緩存比較合理。
在本文正式開始之前,我覺得我們需要先取得以下兩點(diǎn)的共識(shí):
緩存必須要有過期時(shí)間
保證數(shù)據(jù)庫跟緩存的最終一致性即可,不必追求強(qiáng)一致性
為什么必須要有過期時(shí)間?首先對(duì)于緩存來說,當(dāng)它的命中率越高的時(shí)候,我們的系統(tǒng)性能也就越好。如果某個(gè)緩存項(xiàng)沒有過期時(shí)間,而它命中的概率又很低,這就是在浪費(fèi)緩存的空間。而如果有了過期時(shí)間,且在某個(gè)緩存項(xiàng)經(jīng)常被命中的情況下,我們可以在每次命中的時(shí)候都刷新一下它的過期時(shí)間,這樣也就保證了熱點(diǎn)數(shù)據(jù)會(huì)一直在緩存中存在,從而保證了緩存的命中率,提高了系統(tǒng)的性能。
設(shè)置過期時(shí)間還有一個(gè)好處,就是當(dāng)數(shù)據(jù)庫跟緩存出現(xiàn)數(shù)據(jù)不一致的情況時(shí),這個(gè)可以作為一個(gè)最后的兜底手段。也就是說,當(dāng)數(shù)據(jù)確實(shí)出現(xiàn)不一致的情況時(shí),過期時(shí)間可以保證只有在出現(xiàn)不一致的時(shí)間點(diǎn)到緩存過期這段時(shí)間之內(nèi),數(shù)據(jù)庫跟緩存的數(shù)據(jù)是不一致的,因此也保證了數(shù)據(jù)的最終一致性。
那么為什么不應(yīng)該追求數(shù)據(jù)強(qiáng)一致性呢?這個(gè)主要是個(gè)權(quán)衡的問題。數(shù)據(jù)庫跟緩存,以Mysql跟Redis舉例,畢竟是兩套系統(tǒng),如果要保證強(qiáng)一致性,勢必要引入2PC或Paxos等分布式一致性協(xié)議,或者是分布式鎖等等,這個(gè)在實(shí)現(xiàn)上是有難度的,而且一定會(huì)對(duì)性能有影響。而且如果真的對(duì)數(shù)據(jù)的一致性要求這么高,那引入緩存是否真的有必要呢?直接讀寫數(shù)據(jù)庫不是更簡單嗎?那究竟如何做到數(shù)據(jù)庫跟緩存的數(shù)據(jù)強(qiáng)一致性呢?這是個(gè)比較復(fù)雜的問題,本文會(huì)在最后稍作展開。
本文主要在保證最終一致性的前提下進(jìn)行方案討論。
數(shù)據(jù)庫和緩存的讀寫順序
說到數(shù)據(jù)庫和緩存的讀寫順序,最經(jīng)典的方案就是這個(gè)所謂的Cache Aside Pattern了。其實(shí)這個(gè)方案一點(diǎn)也不高大上,基本上我們平時(shí)都在用,只是未必知道名字而已,下面簡單介紹一下這個(gè)方案的思路:
失效:程序先從緩存中讀取數(shù)據(jù),如果沒有命中,則從數(shù)據(jù)庫中讀取,成功之后將數(shù)據(jù)放到緩存中
命中:程序先從緩存中讀取數(shù)據(jù),如果命中,則直接返回
更新:程序先更新數(shù)據(jù)庫,在刪除緩存
前兩步跟數(shù)據(jù)讀取順序有關(guān),我覺得大家對(duì)這樣的設(shè)計(jì)應(yīng)該都沒有異議。讀數(shù)據(jù)的時(shí)候當(dāng)然要優(yōu)先從緩存中讀取,讀不到當(dāng)然要從數(shù)據(jù)庫中讀取,然后還要放到緩存中,否則下次請(qǐng)求過來還得從數(shù)據(jù)庫中讀取。關(guān)鍵問題在于第三點(diǎn),也就是數(shù)據(jù)更新流程,為什么要先更新數(shù)據(jù)庫?為什么之后要?jiǎng)h除緩存而不是更新?這就是本文主要要討論的問題。
總共大概有四種可能的選項(xiàng)(你不可能把數(shù)據(jù)庫刪了吧...):
先更新緩存,再更新數(shù)據(jù)庫
先更新數(shù)據(jù)庫,再更新緩存
先刪除緩存,再更新數(shù)據(jù)庫
先更新數(shù)據(jù)庫,再刪除緩存
接下來我們分情況逐個(gè)討論一下:
先更新緩存,再更新數(shù)據(jù)庫
我們都知道不管是操作數(shù)據(jù)庫還是操作緩存,都有失敗的可能。如果我們先更新緩存,再更新數(shù)據(jù)庫,假設(shè)更新數(shù)據(jù)庫失敗了,那數(shù)據(jù)庫中就存的是老數(shù)據(jù)。當(dāng)然你可以選擇重試更新數(shù)據(jù)庫,那么再極端點(diǎn),負(fù)責(zé)更新數(shù)據(jù)庫的機(jī)器也宕機(jī)了,那么數(shù)據(jù)庫中的數(shù)據(jù)將一直得不到更新,并且當(dāng)緩存失效之后,其他機(jī)器再從數(shù)據(jù)庫中讀到的數(shù)據(jù)是老數(shù)據(jù),然后再放到緩存中,這就導(dǎo)致先前的更新操作被丟失了,因此這么做的隱患是很大的。
從數(shù)據(jù)持久化的角度來說,數(shù)據(jù)庫當(dāng)然要比緩存做的好,我們也應(yīng)當(dāng)以數(shù)據(jù)庫中的數(shù)據(jù)為主,所以需要更新數(shù)據(jù)的時(shí)候我們應(yīng)當(dāng)首先更新數(shù)據(jù)庫,而不是緩存。
先更新數(shù)據(jù)庫,再更新緩存
這里主要有兩個(gè)問題,首先是并發(fā)的問題:假設(shè)線程A(或者機(jī)器A,道理是一樣的)和線程B需要更新同一個(gè)數(shù)據(jù),A先于B但時(shí)間間隔很短,那么就有可能會(huì)出現(xiàn):
線程A更新了數(shù)據(jù)庫
線程B更新了數(shù)據(jù)庫
線程B更新了緩存
線程A更新了緩存
按理說線程B應(yīng)該最后更新緩存,但是可能因?yàn)榫W(wǎng)絡(luò)等原因,導(dǎo)致線程B先于線程A對(duì)緩存進(jìn)行了更新,這就導(dǎo)致緩存中的數(shù)據(jù)不是最新的。
第二個(gè)問題是,我們不確定要更新的這個(gè)緩存項(xiàng)是否會(huì)被經(jīng)常讀取,假設(shè)每次更新數(shù)據(jù)庫都會(huì)導(dǎo)致緩存的更新,有可能數(shù)據(jù)還沒有被讀取過就已經(jīng)再次更新了,這就造成了緩存空間的浪費(fèi)。另外,緩存中的值可能是經(jīng)過一系列計(jì)算的,而并不是直接跟數(shù)據(jù)庫中的數(shù)據(jù)對(duì)應(yīng)的,頻繁更新緩存會(huì)導(dǎo)致大量無效的計(jì)算,造成機(jī)器性能的浪費(fèi)。
綜上所述,更新緩存這一方案是不可取的,我們應(yīng)當(dāng)考慮刪除緩存。
先刪除緩存,再更新數(shù)據(jù)庫
這個(gè)方案的問題也是很明顯的,假設(shè)現(xiàn)在有兩個(gè)請(qǐng)求,一個(gè)是寫請(qǐng)求A,一個(gè)是讀請(qǐng)求B,那么可能出現(xiàn)如下的執(zhí)行序列:
請(qǐng)求A刪除緩存
請(qǐng)求B讀取緩存,發(fā)現(xiàn)不存在,從數(shù)據(jù)庫中讀取到舊值
請(qǐng)求A將新值寫入數(shù)據(jù)庫
請(qǐng)求B將舊值寫入緩存
這樣就會(huì)導(dǎo)致緩存中存的還是舊值,在緩存過期之前都無法讀到新值。這個(gè)問題在數(shù)據(jù)庫讀寫分離的情況下會(huì)更明顯,因?yàn)橹鲝耐叫枰獣r(shí)間,請(qǐng)求B獲取到的數(shù)據(jù)很可能還是舊值,那么寫入緩存中的也會(huì)是舊值。
先更新數(shù)據(jù)庫,再刪除緩存
終于來到我們最常用的方案了,但是最常用并不是說就一定不會(huì)有任何問題,我們依然假設(shè)有兩個(gè)請(qǐng)求,請(qǐng)求A是查詢請(qǐng)求,請(qǐng)求B是更新請(qǐng)求,那么可能會(huì)出現(xiàn)下述情形:
先前緩存剛好失效
請(qǐng)求A查數(shù)據(jù)庫,得到舊值
請(qǐng)求B更新數(shù)據(jù)庫
請(qǐng)求B刪除緩存
請(qǐng)求A將舊值寫入緩存
上述情況確實(shí)有可能出現(xiàn),但是出現(xiàn)的概率可能不高,因?yàn)樯鲜銮樾纬闪⒌臈l件是在讀取數(shù)據(jù)時(shí),緩存剛好失效,并且此時(shí)正好又有一個(gè)并發(fā)的寫請(qǐng)求??紤]到數(shù)據(jù)庫上的寫操作一般都會(huì)比讀操作要慢,(這里指的是在寫數(shù)據(jù)庫時(shí),數(shù)據(jù)庫一般都會(huì)上鎖,而普通的查詢語句是不會(huì)上鎖的。當(dāng)然,復(fù)雜的查詢語句除外,但是這種語句的占比不會(huì)太高)并且聯(lián)系常見的數(shù)據(jù)庫讀寫分離的架構(gòu),可以合理認(rèn)為在現(xiàn)實(shí)生活中,讀請(qǐng)求的比例要遠(yuǎn)高于寫請(qǐng)求,因此我們可以得出結(jié)論。這種情況下緩存中存在臟數(shù)據(jù)的可能性是不高的。
那如果是讀寫分離的場景下呢?如果按照如下所述的執(zhí)行序列,一樣會(huì)出問題:
請(qǐng)求A更新主庫
請(qǐng)求A刪除緩存
請(qǐng)求B查詢緩存,沒有命中,查詢從庫得到舊值
從庫同步完畢
請(qǐng)求B將舊值寫入緩存
如果數(shù)據(jù)庫主從同步比較慢的話,同樣會(huì)出現(xiàn)數(shù)據(jù)不一致的問題。事實(shí)上就是如此,畢竟我們操作的是兩個(gè)系統(tǒng),在高并發(fā)的場景下,我們很難去保證多個(gè)請(qǐng)求之間的執(zhí)行順序,或者就算做到了,也可能會(huì)在性能上付出極大的代價(jià)。那為什么我們還是應(yīng)當(dāng)采用先更新數(shù)據(jù)庫,再刪除緩存這個(gè)策略呢?首先,為什么要?jiǎng)h除而不是更新緩存,這個(gè)在前面有分析,這里不再贅述。那為什么我們應(yīng)當(dāng)先更新數(shù)據(jù)庫呢?因?yàn)榫彺嬖跀?shù)據(jù)持久化這方面往往沒有數(shù)據(jù)庫做得好,而且數(shù)據(jù)庫中的數(shù)據(jù)是不存在過期這個(gè)概念的,我們應(yīng)當(dāng)以數(shù)據(jù)庫中的數(shù)據(jù)為主,緩存因?yàn)橛兄^期時(shí)間這一概念,最終一定會(huì)跟數(shù)據(jù)庫保持一致。
那如果我就是想解決上述說的這兩個(gè)問題,在不要求強(qiáng)一致性的情況下可以怎么做呢?
其他Pattern
Read/Write Through Pattern
我們可以看到,在上面的Cache Aside套路中,我們的應(yīng)用代碼需要維護(hù)兩個(gè)數(shù)據(jù)存儲(chǔ),一個(gè)是緩存(Cache),一個(gè)是數(shù)據(jù)庫(Repository)。所以,應(yīng)用程序比較啰嗦。而Read/Write Through套路是把更新數(shù)據(jù)庫(Repository)的操作由緩存自己代理了,所以,對(duì)于應(yīng)用層來說,就簡單很多了??梢岳斫鉃?,應(yīng)用認(rèn)為后端就是一個(gè)單一的存儲(chǔ),而存儲(chǔ)自己維護(hù)自己的Cache。
Read Through
Read Through 套路就是在查詢操作中更新緩存,也就是說,當(dāng)緩存失效的時(shí)候(過期或LRU換出),Cache Aside是由調(diào)用方負(fù)責(zé)把數(shù)據(jù)加載入緩存,而Read Through則用緩存服務(wù)自己來加載,從而對(duì)應(yīng)用方是透明的。
Write Through
Write Through 套路和Read Through相仿,不過是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒有命中緩存,直接更新數(shù)據(jù)庫,然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己更新數(shù)據(jù)庫(這是一個(gè)同步操作)
Write Behind Caching Pattern
Write Behind 又叫 Write Back。一些了解Linux操作系統(tǒng)內(nèi)核的同學(xué)對(duì)write back應(yīng)該非常熟悉,這不就是Linux文件系統(tǒng)的Page Cache的算法嗎?是的,你看基礎(chǔ)這玩意全都是相通的。所以,基礎(chǔ)很重要,我已經(jīng)不是一次說過基礎(chǔ)很重要這事了。
Write Back套路,一句說就是,在更新數(shù)據(jù)的時(shí)候,只更新緩存,不更新數(shù)據(jù)庫,而我們的緩存會(huì)異步地批量更新數(shù)據(jù)庫。這個(gè)設(shè)計(jì)的好處就是讓數(shù)據(jù)的I/O操作飛快無比(因?yàn)橹苯硬僮鲀?nèi)存嘛 ),因?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è)事)。在軟件設(shè)計(jì)上,我們基本上不可能做出一個(gè)沒有缺陷的設(shè)計(jì),就像算法設(shè)計(jì)中的時(shí)間換空間,空間換時(shí)間一個(gè)道理,有時(shí)候,強(qiáng)一致性和高性能,高可用和高性性是有沖突的。軟件設(shè)計(jì)從來都是取舍Trade-Off。
另外,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。
有沒有更好的思路?
其實(shí)在討論最后一個(gè)方案時(shí),我們沒有考慮操作數(shù)據(jù)庫或者操作緩存可能失敗的情況,而這種情況也是客觀存在的。那么在這里我們簡單討論下,首先是如果更新數(shù)據(jù)庫失敗了,其實(shí)沒有太大關(guān)系,因?yàn)榇藭r(shí)數(shù)據(jù)庫和緩存中都還是老數(shù)據(jù),不存在不一致的問題。假設(shè)刪除緩存失敗了呢?此時(shí)確實(shí)會(huì)存在數(shù)據(jù)不一致的情況。除了設(shè)置緩存過期時(shí)間這種兜底方案之外,如果我們希望盡可能保證緩存可以被及時(shí)刪除,那么我們必須要考慮對(duì)刪除操作進(jìn)行重試。
你當(dāng)然可以直接在代碼中對(duì)刪除操作進(jìn)行重試,但是要知道如果是網(wǎng)絡(luò)原因?qū)е碌氖?,立刻進(jìn)行重試操作很可能也是失敗的,因此在每次重試之間你可能需要等待一段時(shí)間,比如幾百毫秒甚至是秒級(jí)等待。為了不影響主流程的正常運(yùn)行,你可能會(huì)將這個(gè)事情交給一個(gè)異步線程或者線程池來執(zhí)行,但是如果機(jī)器此時(shí)也宕機(jī)了,這個(gè)刪除操作也就丟失了。
那要怎么解決這個(gè)問題呢?首先可以考慮引入消息隊(duì)列,OK我知道寫入消息隊(duì)列一樣可能會(huì)失敗,但是這是建立在緩存跟消息隊(duì)列都不可用的情況下,應(yīng)該說這樣的概率是不高的。引入消息隊(duì)列之后,就由消費(fèi)端負(fù)責(zé)刪除緩存以及重試,可能會(huì)慢一些但是可以保證操作不會(huì)丟失。
回到上述的兩個(gè)問題中去,上述的兩個(gè)問題的核心其實(shí)都在于將舊值寫入了緩存,那么解決這個(gè)問題的辦法其實(shí)就是要將緩存刪除,考慮到網(wǎng)絡(luò)問題導(dǎo)致的執(zhí)行失敗或執(zhí)行順序的問題,這里要進(jìn)行的刪除操作應(yīng)當(dāng)是異步延時(shí)操作。具體來說應(yīng)該怎么做呢?就是參考前面說的,引入消息隊(duì)列,在刪除緩存失敗的情況下,將刪除緩存作為一條消息寫入消息隊(duì)列,然后由消費(fèi)端進(jìn)行慢慢的消費(fèi)和重試。
那如果是讀寫分離場景呢?我們知道數(shù)據(jù)庫(以Mysql為例)主從之間的數(shù)據(jù)同步是通過binlog同步來實(shí)現(xiàn)的,因此這里可以考慮訂閱binlog(可以使用canal之類的中間件實(shí)現(xiàn)),提取出要?jiǎng)h除的緩存項(xiàng),然后作為消息寫入消息隊(duì)列,然后再由消費(fèi)端進(jìn)行慢慢的消費(fèi)和重試。在這種情況下,程序可以不去主動(dòng)刪除緩存,但如果你希望緩存中盡快讀取到最新的值,也可以考慮將緩存刪除,那么就有可能出現(xiàn)又將舊值寫入緩存,且緩存被重復(fù)刪除的情況。但是一般來說這不會(huì)是個(gè)問題,首先舊值重新寫入緩存,情況無非就是又退化到了程序沒有主動(dòng)刪除緩存的這一情況,另外,重復(fù)刪除緩存保證了數(shù)據(jù)庫和緩存之間不會(huì)存在長時(shí)間的數(shù)據(jù)不一致。(為什么刪除了緩存之后,還是有可能將舊值寫入緩存?參見上面先更新數(shù)據(jù)庫,再刪除緩存的方案下,讀寫分離場景下的執(zhí)行序列)當(dāng)然我個(gè)人的建議是,如果你可以忍受一段時(shí)間之內(nèi)的數(shù)據(jù)不一致,那就沒必要自己再主動(dòng)去刪除緩存了。
要解決上述問題的核心就在于要實(shí)現(xiàn)異步延時(shí)刪除這一策略,因此在這里我們需要引入消息隊(duì)列。如果數(shù)據(jù)庫采用讀寫分離架構(gòu),則需要考慮訂閱binlog,否則一樣可能會(huì)出現(xiàn)先刪除,后同步完畢的情況。
緩存擊穿
可能會(huì)有同學(xué)注意到,如果采用刪除緩存的方案,在高并發(fā)場景下可能會(huì)導(dǎo)致緩存擊穿(這個(gè)跟緩存穿透還有點(diǎn)區(qū)別),也就是大量的請(qǐng)求同時(shí)去查詢同一個(gè)緩存,但是這個(gè)緩存又剛好過期或者被刪除了,那么所有的請(qǐng)求全部都會(huì)打到數(shù)據(jù)庫上,導(dǎo)致嚴(yán)重的性能問題。對(duì)于這個(gè)問題包括如何解決緩存穿透,后面我可能會(huì)考慮單獨(dú)寫文章來闡釋一下,這里先簡單說下解決思路,其實(shí)也就是上鎖。
當(dāng)一個(gè)線程需要去訪問這個(gè)緩存的時(shí)候,如果發(fā)現(xiàn)緩存為空,則需要先去競爭一個(gè)鎖,如果成功則進(jìn)行正常的數(shù)據(jù)庫讀取和寫入緩存這一操作,然后再釋放鎖,否則就等待一段時(shí)間之后,重新嘗試讀取緩存,如果還沒有數(shù)據(jù)就繼續(xù)去競爭鎖。這個(gè)是單機(jī)場景,如果有多臺(tái)機(jī)器同時(shí)去訪問同一個(gè)緩存項(xiàng)該怎么辦呢?如果機(jī)器數(shù)不是很多的話,這種情況一般來說也不會(huì)成為一個(gè)問題,不過這里有個(gè)優(yōu)化點(diǎn),就是從數(shù)據(jù)庫讀取到數(shù)據(jù)之后,再對(duì)緩存做一次判斷,如果緩存中已經(jīng)存在數(shù)據(jù),就不需要再寫一遍緩存了。但是如果機(jī)器數(shù)也很多的話,那么就得考慮上分布式鎖了。此方案的問題是顯而易見的,加鎖尤其是加分布式鎖會(huì)對(duì)系統(tǒng)性能有重大影響,而且分布式鎖的實(shí)現(xiàn)非??简?yàn)開發(fā)者的經(jīng)驗(yàn)和實(shí)力,在高并發(fā)場景下這一點(diǎn)顯得尤為重要,因此我建議各位,不到萬不得已的情況下,不要盲目上分布式鎖。
怎么做到強(qiáng)一致性?
可能有同學(xué)就是要來抬杠,現(xiàn)有的這些方案還是不夠完美,如果我就是想要做到強(qiáng)一致性可以怎么做?
上一致性協(xié)議當(dāng)然是可以的,雖然成本也是非??陀^的。2PC甚至是3PC本身是存在一定程度的缺陷的,所以如果要采用這個(gè)方案,那么在架構(gòu)設(shè)計(jì)中要引入很多的容錯(cuò),回退和兜底措施。那如果是上Paxos和Raft呢?那么你首先至少要看過這兩者的相關(guān)論文,并且調(diào)研清楚目前市面上有哪些開源方案,并做好充分的驗(yàn)證,并且能夠做到出了問題自己有能力修復(fù)...對(duì)了,我還沒提到性能問題呢。
那除了一致性協(xié)議以外,有沒有其他的思路?
我們先回到"先更新數(shù)據(jù)庫,再刪除緩存"這個(gè)方案本身上來,從字面上來看,這里有兩步操作,因此在數(shù)據(jù)庫更新之前,到緩存被刪除這段時(shí)間之內(nèi),讀請(qǐng)求讀取到的都是臟數(shù)據(jù)。如果要實(shí)現(xiàn)這兩者的強(qiáng)一致性,只能是在更新完數(shù)據(jù)庫之前,所有的讀請(qǐng)求都必須要被阻塞直到緩存最終被刪除為止。如果是讀寫分離的場景,則要在更新完主庫之前就開始阻塞讀請(qǐng)求,直到主從同步完畢,且緩存被刪除之后才能釋放。
這個(gè)思路其實(shí)就是一種串行化的思路,寫請(qǐng)求一定要在讀請(qǐng)求之前完成,才能保證最新的數(shù)據(jù)對(duì)所有讀請(qǐng)求來說是可見的。說到這里是不是讓你想起了什么?比如volatile,內(nèi)存屏障,ReadWriteLock,或者是數(shù)據(jù)庫的共享鎖,排他鎖...當(dāng)前場景可能不同,但是要面對(duì)的問題都是相似的。
現(xiàn)在回到問題本身,我們要怎么實(shí)現(xiàn)這種阻塞呢?可能有同學(xué)已經(jīng)發(fā)現(xiàn)了,我們需要的其實(shí)是一種 分布式讀寫鎖。對(duì)于寫請(qǐng)求來說,在更新數(shù)據(jù)庫之前,必須要先申請(qǐng)寫鎖,而其他線程或機(jī)器在讀取數(shù)據(jù)之前,必須要先申請(qǐng)讀鎖。讀鎖是共享的,寫鎖是排他的,即如果讀鎖存在,可以繼續(xù)申請(qǐng)讀鎖但無法申請(qǐng)寫鎖,如果寫鎖存在,則無論是讀鎖還是寫鎖都無法申請(qǐng)。只有實(shí)現(xiàn)了這種分布式讀寫鎖,才能保證寫請(qǐng)求在完成數(shù)據(jù)庫和緩存的操作之前,讀請(qǐng)求不會(huì)讀取到臟數(shù)據(jù)。
注意,這里用到的分布式讀寫鎖并沒有解決緩存擊穿的問題,因?yàn)閺淖x請(qǐng)求的視角來看,如果發(fā)生了更新數(shù)據(jù)庫的情況,讀請(qǐng)求要么被阻塞,要么就是緩存為空,需要從數(shù)據(jù)庫讀取數(shù)據(jù)再寫入緩存。為了防止因緩存失效或被刪除導(dǎo)致大量請(qǐng)求直接打到數(shù)據(jù)庫上導(dǎo)致數(shù)據(jù)庫崩潰,你只能考慮加鎖甚至是加分布式鎖。
那么說到分布式讀寫鎖,其實(shí)現(xiàn)一樣有一定的難度。如果確定要使用,我建議使用Curator提供的InterProcessReadWriteLock,或者是Redisson提供的RReadWriteLock。對(duì)分布式讀寫鎖的討論超出了本文的范圍,這里就不做過多展開了。
這里我只提出了我個(gè)人的想法,其他同學(xué)可能還會(huì)有自己的方案,但我相信不管是哪一種,為了要實(shí)現(xiàn)強(qiáng)一致性,系統(tǒng)的性能是一定要付出代價(jià)的,甚至可能會(huì)超出你引入緩存所得到的性能提升。
總結(jié)
在我看來所謂的架構(gòu)設(shè)計(jì),往往是要在眾多的trade-off中選擇最適合當(dāng)前場景的。其實(shí)一旦在方案中使用了緩存,那往往也就意味著我們放棄了數(shù)據(jù)的強(qiáng)一致性,但這也意味著我們的系統(tǒng)在性能上能夠得到一些提升。在如何使用緩存這個(gè)問題上有很多的講究,比如過期時(shí)間的合理設(shè)置,怎么解決或規(guī)避緩存穿透,擊穿甚至是雪崩的問題。后續(xù)有機(jī)會(huì)的話,我會(huì)逐步地闡釋清楚這些問題的來龍去脈,以及如何去解決比較合適。