天天看點

如何保證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實作

如何保證Redis緩存和資料庫的雙寫一緻性?

(1).更新資料庫資料;

(2).删除緩存失敗;

(3).當删除緩存資料失敗時,應用程式發送消息,将需要删除的 Key 發送到MQ中;

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

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

方案二,基于 阿裡Canal 實作

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

如何保證Redis緩存和資料庫的雙寫一緻性?

(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等熱門技術的學習&資料。

如何保證Redis緩存和資料庫的雙寫一緻性?

繼續閱讀