數據庫和redis雙寫一致性


一、前言

目前,企業中大多數數項目中都會用redis做緩存,既然用了緩存,就可能會涉及到redis和數據庫的雙寫,那么就一定會遇到數據一致性問題,我們該怎么解決一致性問題呢?
我想每家企業都會根據自己業務的需要有一套自己的解決方案,下面我們來分析一下常見的方案。

二、Redis做為只讀緩存

2.1 先刪除緩存再更新數據庫

微信圖片_20211025210249

在對數據進行更新的時候先刪除緩存,再更新數據庫,在單線程情況下這個方案不會有問題,但是在並發比較高的情況下就會出現問題了,我們看一下下面這個例子。
假設有A、B兩個請求,請求A做更新操作,請求B做查詢讀取操作:

94E6050A5ED7652046DD9CC6786D002F

  1. 線程A發起一個寫操作,首先刪除緩存。
  2. 此時線程B發起一個讀操作,查詢緩存沒有命中,接着從數據庫讀取結果,設置緩存后返回。
  3. 此時線程A將數據庫更新成新數據。

這樣子就有問題了,緩存和數據庫就不一致了。緩存保存的是老數據,數據庫中存的卻是新數據。往后的查詢操作也都會命中緩存,讀到老數據。

那么這該怎么辦呢?

在線程A更新完數據庫的值之后,我們可以讓它先sleep一小段時間,再進行一次緩存刪除操作。

之所以要sleep,就是為了讓線程B先能夠從數據庫里面讀取數據,把缺失的數據寫入緩存后,線程A再刪除。所有線程Asleep的時間就需要大於線程B讀取數據再寫入緩存的時間。這個時間的值需要對接口讀取和寫緩存的時間進行統計,以此為基礎進行估算。這種方式叫做延遲雙刪

2.2 先更新數據庫再刪除緩存

如果先更新數據庫再刪除緩存是不是就不會存在問題了呢?也不是的,我們再來看一個例子。

如果線程A更新了數據庫中的值,還沒來得及刪除緩存中的值,線程B就開始讀取數據了,那么線程B查詢緩存時,發現命中就會直接 從緩存中讀取舊值。不過,再這種情況下,如果其他線程並發讀緩存請求不多,那么,就不會有很多的請求讀到舊值。所以這種情況對業務影響比較小。

在大多數業務場景中,我們會把redis作為只讀緩存使用。對於只讀緩存,我們既可以先刪除緩存再更新數據庫,也可以先更新數據庫再刪除緩存。我建議優先使用先更新數據庫再更刪除緩存的方法。原因如下

  1. 先刪除緩存再更新數據庫,有可能導致請求因緩存缺失訪問數據庫,給數據造成壓力;
  2. 如果業務應用中讀取數據庫和寫緩存的時間不好估算,那么,延遲雙刪中的等待時間就不好設置。

如果業務層要求必須讀取一致性數據,那么我們就需要在更新數據庫時,現在Redis客戶端暫存並發讀請求,等數據更新完,緩存值刪除后,再讀取數據,從而保證一致性。

2.3 借助消息隊列刪除

這是對先更新數據庫再刪除緩存時,刪除刪除緩存失敗的情況的完善,整個流程如下圖

20200611161911799

流程如下所示
(1)更新數據庫數據;
(2)緩存因為種種問題刪除失敗
(3)將需要刪除的key發送至消息隊列
(4)自己消費消息,獲得需要刪除的key
(5)繼續重試刪除操作,直到成功

然而,該方案有一個缺點,對業務線代碼造成大量的侵入。於是有了方案二,在方案二中,啟動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。

2020061116195754

  • 可以使用阿里的canal將binlog日志采集發送到MQ隊列里面
  • 然后通過ACK機制確認處理這條更新消息,刪除緩存,保證數據緩存一致性

三、Redis做為讀寫緩存

先更新數據庫再更新緩存

這種情況下,如果更新數據庫成功,但是更新緩存失敗,此時數據庫中是最新值,但緩存中是舊值,后續請求直接命名緩存得到舊值。

先更新緩存再更新數據庫

如果數據庫更新失敗,此時緩存中是新值數據庫中是舊值,后續請求命中緩存,但是得到新值,短期內可能影響不大,但是一旦緩存過期或淘汰,讀請求會從數據庫中讀取舊值,並設置到緩存中,之后都會讀取舊值,對業務產生影響。

針對這種其中有一次操作可能失敗的情況,也可以使用重試機制解決,把第二步放入消息隊列中,消費者從消息隊列取出消息,在更新數據庫或緩存,以此達到最終一致性。

以上是沒有並發請求的情況。如果存在並發讀寫,也會產生不一致,分為以下4種場景。

1、先更新數據庫,再更新緩存,寫+讀並發:線程A先更新數據庫,之后線程B讀取數據,此時線程B會命中緩存,讀取到舊值,之后線程A更新緩存成功,后續的讀請求會命中緩存得到最新值。這種場景下,線程A未更新完緩存之前,在這期間的讀請求會短暫讀到舊值,對業務短暫影響。

2、先更新緩存,再更新數據庫,寫+讀並發:線程A先更新緩存成功,之后線程B讀取數據,此時線程B命中緩存,讀取到最新值后返回,之后線程A更新數據庫成功。這種場景下,雖然線程A還未更新完數據庫,數據庫會與緩存存在短暫不一致,但在這之前進來的讀請求都能直接命中緩存,獲取到最新值,所以對業務沒影響。

3、先更新數據庫,再更新緩存,寫+寫並發:線程A和線程B同時更新同一條數據,更新數據庫的順序是先A后B,但更新緩存時順序是先B后A,這會導致數據庫和緩存的不一致。

4、先更新緩存,再更新數據庫,寫+寫並發:與場景3類似,線程A和線程B同時更新同一條數據,更新緩存的順序是先A后B,但是更新數據庫的順序是先B后A,這也會導致數據庫和緩存的不一致。

場景1和2對業務影響較小,場景3和4會造成數據庫和緩存不一致,影響較大。也就是說,在讀寫緩存模式下,寫+讀並發對業務的影響較小,而寫+寫並發時,會造成數據庫和緩存的不一致。

針對場景3和4的解決方案是,對於寫請求,需要配合分布式鎖使用。寫請求進來時,針對同一個資源的修改操作,先加分布式鎖,這樣同一時間只允許一個線程去更新數據庫和緩存,沒有拿到鎖的線程把操作放入到隊列中,延時處理。用這種方式保證多個線程操作同一資源的順序性,以此保證一致性。

綜上,使用讀寫緩存同時操作數據庫和緩存時,因為其中一個操作失敗導致不一致的問題,同樣可以通過消息隊列重試來解決。而在並發的場景下,讀+寫並發對業務沒有影響或者影響較小,而寫+寫並發時需要配合分布式鎖的使用,才能保證緩存和數據庫的一致性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM