MySQL 與 Redis 緩存一致性的實現(xiàn)與挑戰(zhàn)
緩存是提高應(yīng)用性能的重要手段之一,而 MySQL 和 Redis 是兩種常用的數(shù)據(jù)存儲和緩存技術(shù)。在許多應(yīng)用中,常常將 Redis 用作緩存層,以加速對數(shù)據(jù)的訪問。然而,在使用 MySQL 和 Redis 組合時,保持緩存與數(shù)據(jù)庫之間的一致性是一個不得不考慮的問題。
一、緩存一致性的挑戰(zhàn)
MySQL 和 Redis 之間的緩存一致性涉及到兩個方面:
1.數(shù)據(jù)一致性
數(shù)據(jù)在 MySQL 和 Redis 中的一致性是指在對數(shù)據(jù)進(jìn)行更新操作時,確保MySQL 和 Redis 中的數(shù)據(jù)保持同步。如果 Redis 中的緩存數(shù)據(jù)與 MySQL 數(shù)據(jù)庫中的數(shù)據(jù)不一致,可能會導(dǎo)致應(yīng)用程序出現(xiàn)錯誤以及一些未知的問題。
2.緩存有效性
緩存有效性是指 Redis 中的緩存數(shù)據(jù)是否仍然有效,是否需要更新或者過期。如果 Redis 中的緩存數(shù)據(jù)過期,但 MySQL 中的數(shù)據(jù)已經(jīng)更新,可能會導(dǎo)致從 Redis 中獲取到的數(shù)據(jù)不準(zhǔn)確。
再說實現(xiàn)緩存與數(shù)據(jù)庫數(shù)據(jù)一致性的實現(xiàn)方法之前,我們先來了解一下什么是緩存模式?
直接往下看也可以,我在這篇文章里面會重新對幾種緩存模式進(jìn)行一下介紹,并加以配圖說明。
二、緩存模式有哪些
如果你讀過上面緩存模式那篇文章的話,相信你對緩存模式應(yīng)該有一定的了解了,如果不了解也沒關(guān)系,我們一起來看看吧。
1.Cache Aside
最常用的緩存模式,大體意思是先從 cache 中取數(shù)據(jù),沒有獲取到則從數(shù)據(jù)庫中讀取,成功后放到緩存中;
如果在 cache 中獲取到數(shù)據(jù)直接返回;
更新時先把數(shù)據(jù)存到數(shù)據(jù)庫,成功后再讓緩存失效。
(1) 先更新數(shù)據(jù)庫,再更新緩存
遇到的問題是兩個并發(fā)的更新操作,數(shù)據(jù)庫先更新的后更新緩存,數(shù)據(jù)庫后更新的先更新緩存,這樣就會造成數(shù)據(jù)庫與緩存的數(shù)據(jù)不一致,應(yīng)用程序中讀取的數(shù)據(jù)都是臟數(shù)據(jù)
(2) 先刪除緩存,再更新數(shù)據(jù)庫
遇到的問題是有兩個并發(fā)操作,一個更新操作先刪除了緩存,此時另一個并發(fā)的讀取操作沒有命中緩存,直接讀取數(shù)據(jù)庫并更新回了緩存,這個時候正好更新操作完成數(shù)據(jù)更新。此時數(shù)據(jù)庫和緩存的數(shù)據(jù)不一致,應(yīng)用程序讀取的數(shù)據(jù)都臟數(shù)據(jù)了
(3) 先更新數(shù)據(jù)庫,再刪除緩存
這個方式也算是我們實際系統(tǒng)使用中比較推薦的一種方式,但是這種方式在理論上還是可能會出現(xiàn)問題,兩個并發(fā)操作,其中一個查詢操作沒有命中緩存,此時查詢出來了數(shù)據(jù)庫中的老數(shù)據(jù),此時另一個并發(fā)的更新操作,在剛才的并發(fā)讀操作之后更新了數(shù)據(jù)庫中的數(shù)據(jù)并刪除了緩存,然后并發(fā)讀操作線程又把老數(shù)據(jù)寫入了緩存,此時又造成了數(shù)據(jù)的不一致,應(yīng)用程序讀取的都是臟數(shù)據(jù)。因為這種概率差生的情況實在是太小,所以才是我們系統(tǒng)中經(jīng)常使用的一種方式了。
2.Read/Write Through
Cache Aside 模式中,應(yīng)用程序需要維護(hù)兩個數(shù)據(jù)存儲,一個是緩存,一個是數(shù)據(jù)庫,在 Read/Write Through 更新模式中,應(yīng)用程序只需要維護(hù)緩存,數(shù)據(jù)庫的維護(hù)工作就有緩存代理了
(1) Read Through
Read Through 模式就是在查詢時更新緩存,也就是說,在緩存失效時,Cache Aside 模式是由調(diào)用方負(fù)責(zé)把數(shù)據(jù)載入緩存,而 Read Through 模式是緩存服務(wù)自己更新緩存,自己來加載數(shù)據(jù)。
當(dāng)應(yīng)用程序執(zhí)行讀操作時,如果緩存中不存在所需數(shù)據(jù),則緩存會自動從數(shù)據(jù)源(如數(shù)據(jù)庫)中讀取數(shù)據(jù),并將數(shù)據(jù)加載到緩存中,然后返回給應(yīng)用程序。
Read-Through 策略減少了應(yīng)用程序與數(shù)據(jù)源之間的直接交互次數(shù),提高了讀操作的性能和響應(yīng)速度。
(2) Write Through
Write Through 和 Read Through 類似,當(dāng)數(shù)據(jù)更新時,如果命中緩存則更新緩存,然后緩存更新數(shù)據(jù)庫,這是一個同步的操作;如果沒有命中緩存,直接更新數(shù)據(jù)庫返回。
Write-Through 策略保證了緩存和數(shù)據(jù)源中的數(shù)據(jù)一致性,但由于每次寫操作都需要等待數(shù)據(jù)源的確認(rèn),可能會影響寫操作的性能和延遲。
3. Write Behind Caching
Write Behind Caching 更新模式是在更新數(shù)據(jù)時只更新緩存,不更新數(shù)據(jù)庫,而我們的緩存會異步的更新數(shù)據(jù)庫。這個模式的話就是速度快,畢竟我們直接操作內(nèi)存,因為是異步的,Write Behind Caching 更新模式還可以合并對同一個數(shù)據(jù)的多次操作到數(shù)據(jù)庫,所以性能的提升也是很明顯的
問題就是數(shù)據(jù)不是強(qiáng)一致性的,而且還可能會丟失,Write Behind Caching 更新模式實現(xiàn)邏輯復(fù)雜,因為它需要確認(rèn)有哪些數(shù)據(jù)是被更新的,哪些數(shù)據(jù)是需要刷到持久層的數(shù)據(jù)庫的。只有當(dāng)緩存失效的時候才會把它真正的持久化起來。
Write-Behind 策略提高了寫操作的性能和響應(yīng)速度,但在寫入緩存后,數(shù)據(jù)源中的數(shù)據(jù)可能會落后于緩存中的數(shù)據(jù)一段時間,存在一定的數(shù)據(jù)一致性風(fēng)險。
三、一致性有哪些
說到一致性,我們應(yīng)該想到的就是分布式系統(tǒng)中多個節(jié)點(diǎn)對看到的數(shù)據(jù)副本都保持一致的特性。換個說法就是,無論用戶在哪個節(jié)點(diǎn)上執(zhí)行操作,最終所有節(jié)點(diǎn)上的數(shù)據(jù)是相同的,且滿足一定的約束條件。
在分布式系統(tǒng)中,實現(xiàn)一致性是很困難的,因為系統(tǒng)中的多個節(jié)點(diǎn)可能會因為網(wǎng)絡(luò)延遲,節(jié)點(diǎn)故障或者其他的因素導(dǎo)致多個數(shù)據(jù)副本之間的狀態(tài)不一致。為了實現(xiàn)分布式系統(tǒng)中的一致性,業(yè)界常用的算法和協(xié)議有 Paxos、Raft、ZAB。
分布式系統(tǒng)的一致性又分為強(qiáng)一致性、弱一致性、最終一致性。
- 強(qiáng)一致性:最嚴(yán)格的一致性,相當(dāng)于對于用戶來說是最友好的。因為這個相當(dāng)于系統(tǒng)寫入的是什么數(shù)據(jù),讀取時也就是什么數(shù)據(jù)。
- 弱一致性:相比于強(qiáng)一致性,弱一致性不承諾立即讀取最新的值,也不承諾多久之后數(shù)據(jù)一致。但是會盡可能的保證在到達(dá)某個時間點(diǎn)之后,數(shù)據(jù)達(dá)到一致狀態(tài)。
- 最終一致性:最終一致性是弱一致性的一個特例,保證在一定時間之后達(dá)到數(shù)據(jù)一致狀態(tài),因此最終一致性也是目前業(yè)界來說最推崇的模型。
四、一致性實現(xiàn)方法
1.雙寫
雙寫其實就是 Write Through 模式,在寫入 MySQL 數(shù)據(jù)庫的同時,立即寫入 Redis 緩存。這樣可以確保 MySQL 和 Redis 中的數(shù)據(jù)保持一致,但增加了寫入的延遲,并且增加了系統(tǒng)復(fù)雜度。
(1) 雙寫為什么先操作數(shù)據(jù)庫在操作緩存?
我們來看如下的例子,線程A 與 線程B 是一組并發(fā)請求。
- 線程A 發(fā)起一個寫請求、先刪除 Redis 中的緩存。
- 線程B 發(fā)起一個讀請求,沒有命中緩存。
- 線程B 讀取數(shù)據(jù)庫,獲取數(shù)據(jù)。
- 線程B 寫入老數(shù)據(jù)到緩存。
- 線程A 寫入DB 新數(shù)據(jù)。
到這就發(fā)現(xiàn)了問題了吧,如果你沒發(fā)現(xiàn),那就跟著我的思路來看一下。
大家來看第三步,線程B 去讀取數(shù)據(jù)庫,此時 線程A 是還沒有寫入新數(shù)據(jù)的,所以此時 線程B 讀取的數(shù)據(jù)是老數(shù)據(jù)。
而第5步,線程A 往數(shù)據(jù)庫寫入的才是最新的數(shù)據(jù)。
所以此時也就造成了數(shù)據(jù)不一致 了。
對于這種情況造成的臟數(shù)據(jù)緩存,有的小伙伴可能就提出來了,可以使用緩存雙刪啊,那么我們來繼續(xù)往下看。
(2) 緩存延遲雙刪
延遲雙刪,就是字面意思,刪除兩邊緩存。你知道問題在哪嗎,想一想?
。。。。。。
雙刪,有可能刪除失敗嗎?
刪除失敗怎么辦?
雙刪的步驟如下:
- 刪除緩存。
- 更新數(shù)據(jù)庫。
- 延遲刪除(此時延遲的差不多是讀請求的耗時多一點(diǎn),防止讀請求設(shè)置臟數(shù)據(jù)緩存)。
(3) 刪除緩存的重試機(jī)制
刪除緩存失敗,第一個想到的應(yīng)該是,刪除失敗了就多刪除幾次吧。
所以我們可以借助于消息隊列,將刪除失敗的 key 加入到消息隊列,對消息進(jìn)行重試刪除操作。
既然我們都用到消息隊列了,那么我們?yōu)槭裁床恢苯颖O(jiān)聽 binlog 實現(xiàn)異步刪除緩存呢。
2.消息隊列
使用消息隊列監(jiān)聽數(shù)據(jù)庫變更事件、異步更新緩存。能夠保證在數(shù)據(jù)庫更新后,緩存能夠按照順序進(jìn)行更新。
此時的例子就是,使用 Canal 監(jiān)聽 binlog ,發(fā)送數(shù)據(jù)到 MQ 中,應(yīng)用程序監(jiān)聽 MQ 消息實現(xiàn) Redis 緩存的更新。
五、總結(jié)
實現(xiàn) MySQL 與 Redis緩存的一致性中,需要考慮很多的方面,最直觀的就是業(yè)務(wù)場景,性能要求,以及數(shù)據(jù)的安全等因素綜合考慮。
例如,對于讀多寫少的場景,可以采用 Read/Write Through 模式;
而對于寫操作頻繁的場景,則可能需要考慮 Write Behind Caching 模式。