如何保證Redis緩存和數據庫的雙寫一致性?

在數據庫+緩存模式下,當數據庫中的數據需要更新時,緩存裏的數據怎麼處理?如何保證緩存和數據庫中數據的一致性?常用的解決方案有兩種(其他渣渣的方案這裏不討論):

1、先刪除緩存,再更新數據庫;

2、先更新數據庫,再刪除緩存;

下面我們就來看一下這兩種方案,看看它們是怎麼保證數據一致性的?

一、先刪除緩存,再更新數據庫

理想的流程是這樣的:先刪除緩存,再更新數據庫,更新完數據庫後,當有請求進來的時候發現緩存中沒有數據,於是去查數據庫,讀取到更新後的新數據放回緩存再返回。這只是理想化的流程,如果只是簡單的這樣做,我們看看會存在什麼問題呢?

在高併發場景下,假設有兩個線程,線程A先刪除緩存,再去更新數據庫,在線程A刪除緩存成功但更新數據庫還未提交的時候,進來了一個線程B讀取數據,發現緩存中沒有數據,於是去讀數據庫,這時B讀到的是舊數據,然後再將這個舊數據放回緩存,等A更新數據庫完成以後,數據庫和緩存中的數據就是不一致的。如果該緩存還沒有設置過期時間,那這個數據將一直髒下去。

這個問題可以用"延時雙刪策略"來解決,A線程先刪除緩存,再更新數據庫,數據庫更新完成後休眠200ms,再次刪除緩存,這樣做的目的就是保證中間產生的髒數據最後被再次刪除。但這個200ms要根據自己的業務情況來確定。

還有一個問題,在延時雙刪策略第二次刪除緩存的時候刪除失敗怎麼辦?這種情況可以提供一個"重試保障機制",如果刪除失敗,可以將刪除失敗的key發送到消息隊列,再次重試操作。

二、先更新數據庫,再刪除緩存

理想的流程是這樣的:先更新數據庫,再刪除緩存,當再有請求進來的時候發現緩存中沒有數據,於是去查數據庫,再將更新後的新數據放到緩存返回。

這種策略就是典型的“Cache Aside 模式”,更新緩存的設計模式有四種:Cache aside, Read through, Write through和Write behind caching。這四種模式並不僅僅適用於數據庫和緩存之間的更新,它們設計的初衷是基於計算機體系結構的,比如CPU的緩存,硬盤文件系統中的緩存,硬盤上的緩存,數據庫中的緩存等。所以,它們都是非常權威的,而且歷經了長時間考驗的最佳實踐,我們直接遵從就可以了,沒必要重複造輪子。

推薦閱讀:《緩存更新的套路-https://coolshell.cn/articles/17416.html》

FaceBook在論文《Scaling Memcache at Facebook》中說過,他們採用的就是Cache Aside 策略。

※ 先更新數據庫,再刪除緩存有什麼問題?

1、併發問題

從理論上來講,Cache Aside也有可能發生併發問題,假設有兩個線程,A線程讀取數據,沒有命中緩存,然後就去查數據庫,在A查詢數據庫還沒有返回結果的時候,另一個線程B執行一個寫操作,更新完數據庫後,讓緩存失效(實際上這時候緩存已經是失效的,因爲A在讀的時候就沒有命中緩存),然後,之前的線程A讀取數據庫返回了結果,再把老的數據放進緩存,這時候緩存中放得是A讀出來的老數據,而數據庫中存的是B更新後的新數據,數據庫和緩存數據不一致。

這種情況理論上會出現,不過,實際上出現的概率可能非常非常低。因爲上述併發場景的出現,需要同時具備條件:1、某線程在讀緩存時緩存失效了,而且剛好併發着有一個寫操作;2、讀操作在寫操作之前開始,在寫操作之後結束,也就是說讀操作的耗時大於寫操作;出現這種場景的概率還是很低的。

如果要解決Cache Aside的併發問題,可以通過2PC或是Paxos協議保證一致性,或者盡力的降低併發時髒數據的概率,而Facebook使用的就是降低概率的玩法,因爲2PC太慢,而Paxos太複雜。

2、數據庫更新成功,刪除緩存失敗怎麼辦?

如果發生這種情況,數據庫中存放的是更新後的數據,緩存因爲沒有刪除成功存放的還是老數據,這個問題怎麼解決呢,我們可以提供一種保障性的"重試機制"。

方案一、基於MO實現

(1).更新數據庫數據;

(2).刪除緩存失敗;

(3).當刪除緩存數據失敗時,應用程序發送消息,將需要刪除的 Key 發送到MQ中;

(4).應用程序自己消費消息;

(5).應用接收到消息後,再次嘗試刪除緩存,如果再次刪除失敗,可重發消息多次嘗試;

方案二,基於 阿里Canal 實現

Canal是阿里開發的基於數據庫增量日誌解析,提供增量數據的訂閱&消費的中間件,目前主要支持MySQL的binlog解析。從下圖可以看出,基於Canal的實現方案完全避免了對業務代碼的侵入,核心業務代碼只管更新數據庫,其他的不用care。

 

(1).更新數據庫數據;

(2).MySQL 將數據更新日誌寫入 binlog 中;

(3).Canal 訂閱 & 消費 MySQL binlog;

(4).Canal 解析binlog,提取出​更新數據的key發送給另一段非業務代碼;

(5).非業務代碼嘗試刪除緩存操作,發現刪除失敗;

(6).將需要刪除緩存的 Key 發送到消息隊列 (MQ) 中;

(7).消費消息,從隊列中拿到要刪除的緩存key;

(8).拿到要刪除的key後,再次嘗試刪除緩存,如果再次刪除失敗,可重發消息多次嘗試;

總的來說就是先更新數據庫,再刪除緩存,提供一個"重試保障機制",如果刪除緩存失敗時,可以將刪除失敗的key發送到消息隊列,再進行重試刪除操作。 

 

感興趣的小夥伴可以關注一下博主的公衆號,1W+技術人的選擇,致力於原創技術乾貨,包含Redis、RabbitMQ、Kafka、SpringBoot、SpringCloud、ELK等熱門技術的學習&資料。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章