如何保證緩存和數(shù)據(jù)庫(kù)的一致性?
文章目錄
1. 問(wèn)題分析
2. Cache-Aside
2.1 讀緩存
2.2 寫(xiě)緩存
2.3 延遲雙刪
2.4 如何確保原子性
3. Read-Through/Write-Through
3.1 Read-Through
3.2 Write-Through
4. Write Behind
如果你對(duì)這個(gè)問(wèn)題有過(guò)研究,應(yīng)該可以發(fā)現(xiàn)這個(gè)問(wèn)題其實(shí)很好回答,如果第一次聽(tīng)到或者第一次遇到這個(gè)問(wèn)題,估計(jì)會(huì)有點(diǎn)懵,今天我們來(lái)聊聊這個(gè)話(huà)題。
1. 問(wèn)題分析
首先我們來(lái)看看為什么會(huì)有這個(gè)問(wèn)題!
我們?cè)谌粘i_(kāi)發(fā)中,為了提高數(shù)據(jù)響應(yīng)速度,可能會(huì)將一些熱點(diǎn)數(shù)據(jù)保存在緩存中,這樣就不用每次都去數(shù)據(jù)庫(kù)中查詢(xún)了,可以有效提高服務(wù)端的響應(yīng)速度,那么目前我們最常使用的緩存就是 Redis 了。
用 Redis 做緩存,并不是一說(shuō)緩存就是 Redis,還是要結(jié)合業(yè)務(wù)的具體情況,我們可以根據(jù)不同業(yè)務(wù)對(duì)數(shù)據(jù)要求的實(shí)時(shí)性不同,將數(shù)據(jù)分為三級(jí),以電商項(xiàng)目為例:
第 1 級(jí):訂單數(shù)據(jù)和支付流水?dāng)?shù)據(jù):這兩塊數(shù)據(jù)對(duì)實(shí)時(shí)性和精確性要求很高,所以一般是不需要添加緩存的,直接操作數(shù)據(jù)庫(kù)即可。
第 2 級(jí):用戶(hù)相關(guān)數(shù)據(jù):這些數(shù)據(jù)和用戶(hù)相關(guān),具有讀多寫(xiě)少的特征,所以我們使用 redis 進(jìn)行緩存。
第 3 級(jí):支付配置信息:這些數(shù)據(jù)和用戶(hù)無(wú)關(guān),具有數(shù)據(jù)量小,頻繁讀,幾乎不修改的特征,所以我們使用本地內(nèi)存進(jìn)行緩存。
選中合適的數(shù)據(jù)存入 Redis 之后,接下來(lái),每當(dāng)要讀取數(shù)據(jù)的時(shí)候,就先去 Redis 中看看有沒(méi)有,如果有就直接返回;如果沒(méi)有,則去數(shù)據(jù)庫(kù)中讀取,并且將從數(shù)據(jù)庫(kù)中讀取到的數(shù)據(jù)緩存到 Redis 中,大致上就是這樣一個(gè)流程,讀取數(shù)據(jù)的這個(gè)流程實(shí)際上是比較清晰也比較簡(jiǎn)單的,沒(méi)啥好說(shuō)的。
然而,當(dāng)數(shù)據(jù)存入緩存之后,如果需要更新的話(huà),往往會(huì)來(lái)帶另外的問(wèn)題:
當(dāng)有數(shù)據(jù)需要更新的時(shí)候,先更新緩存還是先更新數(shù)據(jù)庫(kù)?如何確保更新緩存和更新數(shù)據(jù)庫(kù)這兩個(gè)操作的原子性?
更新緩存的時(shí)候該怎么更新?修改還是刪除?
怎么辦?正常來(lái)說(shuō),我們有四種方案:
先更新緩存,再更新數(shù)據(jù)庫(kù)。
先更新數(shù)據(jù)庫(kù),再更新緩存。
先淘汰緩存,再更新數(shù)據(jù)庫(kù)。
先更新數(shù)據(jù)庫(kù),再淘汰緩存。
到底使用哪種?
在回答這個(gè)問(wèn)題之前,我們不妨先來(lái)看看三個(gè)經(jīng)典的緩存模式:
Cache-Aside
Read-Through/Write through
Write Behind
2. Cache-Aside
Cache-Aside,中文也叫旁路緩存模式,如果我們能夠在項(xiàng)目中采用 Cache-Aside,那么就能夠盡可能的解決緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致的問(wèn)題,注意是盡可能的解決,并無(wú)法做到絕對(duì)解決。
Cache-Aside 又分為讀緩存和寫(xiě)緩存兩種情況,我們分別來(lái)看。
2.1 讀緩存
先來(lái)看一張流程圖:
它的流程是這樣:
讀取數(shù)據(jù)。
檢查緩存中是否有需要的數(shù)據(jù),如果命中緩存(Cache Hit),則直接返回?cái)?shù)據(jù)。
如果沒(méi)有命中緩存,即 Cache Miss,那么就先去訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)。
將從數(shù)據(jù)庫(kù)中讀取到的數(shù)據(jù)設(shè)置到緩存中。
返回?cái)?shù)據(jù)。
這是 Cache-Aside 的讀緩存流程。
其實(shí)對(duì)于讀緩存的流程而言,大家一般都沒(méi)什么異議,有異議的主要是寫(xiě)流程,我們繼續(xù)來(lái)看。
2.2 寫(xiě)緩存
先來(lái)看一張流程圖:
這個(gè)寫(xiě)緩存的流程就比較簡(jiǎn)單,先更新數(shù)據(jù)庫(kù)中的數(shù)據(jù),然后刪除舊的緩存即可。
流程雖然簡(jiǎn)單,但是卻引伸出來(lái)兩個(gè)問(wèn)題:
為什么是刪除舊緩存而不是更新舊緩存?
為什么不先刪除舊的緩存,然后再更新數(shù)據(jù)庫(kù)?
我們來(lái)分別回答這兩個(gè)問(wèn)題。
為什么是刪除舊緩存而不是更新舊緩存?
更新緩存,說(shuō)著容易做起來(lái)并不容易。很多時(shí)候我們更新緩存并不是簡(jiǎn)簡(jiǎn)單單更新一個(gè) Bean。很多時(shí)候,我們緩存的都是一些復(fù)雜操作或者計(jì)算(例如大量聯(lián)表操作、一些分組計(jì)算)的結(jié)果,如果不加緩存,不但無(wú)法滿(mǎn)足高并發(fā)量,同時(shí)也會(huì)給 MySQL 數(shù)據(jù)庫(kù)帶來(lái)巨大的負(fù)擔(dān)。那么對(duì)于這樣的緩存,更新起來(lái)實(shí)際上并不容易,此時(shí)選擇刪除緩存效果會(huì)更好一些。
對(duì)于一些寫(xiě)頻繁的應(yīng)用,如果按照更新緩存->更新數(shù)據(jù)庫(kù)的模式來(lái),比較浪費(fèi)性能,因?yàn)槭紫葘?xiě)緩存很麻煩,其次每次都要寫(xiě)緩存,但是可能寫(xiě)了十次,只讀了一次,讀的時(shí)候讀到的緩存數(shù)據(jù)是第十次的,前面九次寫(xiě)緩存都是無(wú)效的,對(duì)于這種情況不如采取先寫(xiě)數(shù)據(jù)庫(kù)再刪除緩存的策略。
在多線(xiàn)程環(huán)境下,這樣的更新策略還有可能會(huì)導(dǎo)致數(shù)據(jù)邏輯錯(cuò)誤,來(lái)看如下一張流程圖:
可以看到,有兩個(gè)并發(fā)的線(xiàn)程 A 和 B:
首先 A 線(xiàn)程更新了數(shù)據(jù)庫(kù)。
接下來(lái) B 線(xiàn)程更新了數(shù)據(jù)庫(kù)。
由于網(wǎng)絡(luò)等原因,B 線(xiàn)程先更新了緩存。
A 線(xiàn)程更新了緩存。
那么此時(shí),緩存中保存的數(shù)據(jù)就是不正確的,而如果采用了刪除緩存的方式,就不會(huì)發(fā)生這種問(wèn)題了。
為什么不先刪除舊的緩存,然后再更新數(shù)據(jù)庫(kù)?
這個(gè)也是考慮到并發(fā)請(qǐng)求,假設(shè)我們先刪除舊的緩存,然后再更新數(shù)據(jù)庫(kù),那么就有可能出現(xiàn)如下這種情況:
這個(gè)操作是這樣的,有兩個(gè)線(xiàn)程,A 和 B,其中 A 寫(xiě)數(shù)據(jù),B 讀數(shù)據(jù),具體流程如下:
A 線(xiàn)程首先刪除緩存。
B 線(xiàn)程讀取緩存,發(fā)現(xiàn)緩存中沒(méi)有數(shù)據(jù)。
B 線(xiàn)程讀取數(shù)據(jù)庫(kù)。
B 線(xiàn)程將從數(shù)據(jù)庫(kù)中讀取到的數(shù)據(jù)寫(xiě)入緩存。
A 線(xiàn)程更新數(shù)據(jù)庫(kù)。
一套操作下來(lái),我們發(fā)現(xiàn)數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)不一致了!所以,在 Cache-Aside 中是先更新數(shù)據(jù)庫(kù),再刪除緩存。
2.3 延遲雙刪
其實(shí)無(wú)論是先更新數(shù)據(jù)庫(kù)再刪除緩存,還是先刪除緩存再更新數(shù)據(jù)庫(kù),在并發(fā)環(huán)境下都有可能存在問(wèn)題:
假設(shè)有 A、B 兩個(gè)并發(fā)請(qǐng)求:
先更新數(shù)據(jù)庫(kù)再刪除緩存:當(dāng)請(qǐng)求 A 更新數(shù)據(jù)庫(kù)之后,還未來(lái)得及進(jìn)行緩存清除,此時(shí)請(qǐng)求 B 查詢(xún)到并使用了 Cache 中的舊數(shù)據(jù)。
先刪除緩存再更新數(shù)據(jù)庫(kù):當(dāng)請(qǐng)求 A 執(zhí)行清除緩存后,還未進(jìn)行數(shù)據(jù)庫(kù)更新,此時(shí)請(qǐng)求 B 進(jìn)行查詢(xún),查到了舊數(shù)據(jù)并寫(xiě)入了 Cache。
當(dāng)然我們前面已經(jīng)分析過(guò)了,盡量先操作數(shù)據(jù)庫(kù)再操作緩存,但是即使這樣也還是有可能存在問(wèn)題,解決問(wèn)題的辦法就是延遲雙刪。
延遲雙刪是這樣:先執(zhí)行緩存清除操作,再執(zhí)行數(shù)據(jù)庫(kù)更新操作,延遲 N 秒之后再執(zhí)行一次緩存清除操作,這樣就不用擔(dān)心緩存中的數(shù)據(jù)和數(shù)據(jù)庫(kù)中的數(shù)據(jù)不一致了。
那么這個(gè)延遲 N 秒,N 是多大比較合適呢?一般來(lái)說(shuō),N 要大于一次寫(xiě)操作的時(shí)間,如果延遲時(shí)間小于寫(xiě)入緩存的時(shí)間,會(huì)導(dǎo)致請(qǐng)求 A 已經(jīng)延遲清除了緩存,但是此時(shí)請(qǐng)求 B 緩存還未寫(xiě)入,具體是多少,就要結(jié)合自己的業(yè)務(wù)來(lái)統(tǒng)計(jì)這個(gè)數(shù)值了。
2.4 如何確保原子性
但是更新數(shù)據(jù)庫(kù)和刪除緩存畢竟不是一個(gè)原子操作,要是數(shù)據(jù)庫(kù)更新完畢后,刪除緩存失敗了咋辦?
對(duì)于這種情況,一種常見(jiàn)的解決方案就是使用消息中間件來(lái)實(shí)現(xiàn)刪除的重試。大家知道,MQ 一般都自帶消費(fèi)失敗重試的機(jī)制,當(dāng)我們要?jiǎng)h除緩存的時(shí)候,就往 MQ 中扔一條消息,緩存服務(wù)讀取該消息并嘗試刪除緩存,刪除失敗了就會(huì)自動(dòng)重試。如果小伙伴們還不懂 RabbitMQ 的使用,可以在公眾號(hào)江南一點(diǎn)雨后臺(tái)回復(fù) rabbitmq,有免費(fèi)的視頻+文檔。
3. Read-Through/Write-Through
這種緩存操作模式,松哥印象最深的是在 Oracle Coherence 中有應(yīng)用,不知道小伙伴們有沒(méi)有用過(guò) Oracle Coherence,這是一個(gè)內(nèi)存數(shù)據(jù)網(wǎng)格,通過(guò)這個(gè),應(yīng)用開(kāi)發(fā)人員和管理人員可快速訪(fǎng)問(wèn)鍵值數(shù)據(jù),Coherence 可提供集群式低延遲數(shù)據(jù)存儲(chǔ)、多語(yǔ)言網(wǎng)格計(jì)算和異步事件流處理,從而為客戶(hù)企業(yè)應(yīng)用賦予超高水平的可擴(kuò)展性和性能。
Oracle Coherence 我們就不討論了,我們就來(lái)說(shuō)說(shuō) Read-Through。
3.1 Read-Through
這里為了省事,我就不自己畫(huà)圖了,網(wǎng)上找了一張圖片,如下:
乍一看,很多人感覺(jué)這和 Cache-Aside 一樣呀,沒(méi)啥區(qū)別!是的,單看流程是不太容易看到區(qū)別。
Read-Through 是一種類(lèi)似于 Cache-Aside 的緩存方法,區(qū)別在于,在 Cache-Aside 中,由應(yīng)用程序決定去讀取緩存還是讀取數(shù)據(jù)庫(kù),這樣就會(huì)導(dǎo)致應(yīng)用程序中出現(xiàn)了很多業(yè)務(wù)無(wú)關(guān)的代碼;而在 Read-Through 中,相當(dāng)于多出來(lái)了一個(gè)中間層 Cache Middleware,由它去讀取緩存或者數(shù)據(jù)庫(kù),應(yīng)用層的代碼得到了簡(jiǎn)化,松哥之前寫(xiě)過(guò) Spring Cache 的用法,大家回憶下 Spring Cache 中的 @Cacheable 注解,感覺(jué)像不像 Read-Through?
我畫(huà)一個(gè)簡(jiǎn)單的流程圖大家來(lái)看下:
可以看到,和 Cache-Aside 相比,其實(shí)就相當(dāng)于是多了一個(gè) Cache Middleware,這樣我們?cè)趹?yīng)用程序中就只需要正常的讀寫(xiě)數(shù)據(jù)就行了,并不用管底層的具體邏輯,相當(dāng)于把緩存相關(guān)的代碼從應(yīng)用程序中剝離出來(lái)了,應(yīng)用程序只需要專(zhuān)注于業(yè)務(wù)就行了。
3.2 Write-Through
Write-Through 其實(shí)也是差不多,所有的操作都交給 Cache Middleware 來(lái)完成,應(yīng)用程序中就是一句簡(jiǎn)單的更新就行了,我們來(lái)看看流程:
在 Write-Through 策略中,所有的寫(xiě)操作都經(jīng)過(guò) Cache Middleware,每次寫(xiě)入時(shí),Cache Middleware 會(huì)將數(shù)據(jù)存儲(chǔ)在 DB 和 Cache 中,這兩個(gè)操作發(fā)生在一個(gè)事務(wù)中,因此,只有兩個(gè)都寫(xiě)入成功,一切才會(huì)成功。
這種寫(xiě)數(shù)據(jù)的優(yōu)勢(shì)在于,應(yīng)用程序只與 Cache Middleware 對(duì)話(huà),所以它的代碼更加干凈和簡(jiǎn)單。
4. Write Behind
Write-Behind 緩存策略類(lèi)似于 Write-Through 緩存,應(yīng)用程序僅與 Cache Middleware 通信,Cache Middleware 會(huì)預(yù)留一個(gè)與應(yīng)用程序通信的接口。
Write-Behind 與 Write-Through 最大的區(qū)別在于,前者是數(shù)據(jù)首先寫(xiě)入緩存,一段時(shí)間后(或通過(guò)其他觸發(fā)器)再將數(shù)據(jù)寫(xiě)入 Database,并且這里涉及到的寫(xiě)入是一個(gè)異步操作。這種方式下,Cache 和 DB 數(shù)據(jù)的一致性不強(qiáng),對(duì)一致性要求高的系統(tǒng)要謹(jǐn)慎使用,如果有人在數(shù)據(jù)尚未寫(xiě)入數(shù)據(jù)源的情況下直接從數(shù)據(jù)源獲取數(shù)據(jù),則可能導(dǎo)致獲取過(guò)期數(shù)據(jù),不過(guò)對(duì)于頻繁寫(xiě)入的場(chǎng)景,這個(gè)其實(shí)非常適用。
將數(shù)據(jù)寫(xiě)入 DB 可以通過(guò)多種方式完成:
一種是收集所有寫(xiě)入操作,然后在某個(gè)時(shí)間點(diǎn)(例如,當(dāng) DB 負(fù)載較低時(shí))對(duì)數(shù)據(jù)源進(jìn)行批量寫(xiě)入。
另一種方法是將寫(xiě)入合并成更小的批次,例如每次收集五個(gè)寫(xiě)入操作,然后對(duì)數(shù)據(jù)源進(jìn)行批量寫(xiě)入。
這個(gè)流程圖就不想畫(huà)了,在網(wǎng)上找了一張,小伙伴們參考下:
好啦,和小伙伴們簡(jiǎn)單聊了下雙寫(xiě)一致性的問(wèn)題,有問(wèn)題歡迎留言討論。
參考資料:
https://www.jianshu.com/p/a8eb1412471f
https://catsincode.com/caching-strategy/
數(shù)據(jù)庫(kù)
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。