1. 程式人生 > 其它 >資料庫和redis雙寫一致性

資料庫和redis雙寫一致性

一、前言

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

二、Redis做為只讀快取

2.1 先刪除快取再更新資料庫

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

  1. 執行緒A發起一個寫操作,首先刪除快取。
  2. 此時執行緒B發起一個讀操作,查詢快取沒有命中,接著從資料庫讀取結果,設定快取後返回。
  3. 此時執行緒A將資料庫更新成新資料。

這樣子就有問題了,快取和資料庫就不一致了。快取儲存的是老資料,資料庫中存的卻是新資料。往後的查詢操作也都會命中快取,讀到老資料。

那麼這該怎麼辦呢?

線上程A更新完資料庫的值之後,我們可以讓它先sleep一小段時間,再進行一次快取刪除操作。

之所以要sleep,就是為了讓執行緒B先能夠從資料庫裡面讀取資料,把缺失的資料寫入快取後,執行緒A再刪除。所有執行緒Asleep的時間就需要大於執行緒B讀取資料再寫入快取的時間。這個時間的值需要對介面讀取和寫快取的時間進行統計,以此為基礎進行估算。這種方式叫做延遲雙刪

2.2 先更新資料庫再刪除快取

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

如果執行緒A更新了資料庫中的值,還沒來得及刪除快取中的值,執行緒B就開始讀取資料了,那麼執行緒B查詢快取時,發現命中就會直接 從快取中讀取舊值。不過,再這種情況下,如果其他執行緒併發讀快取請求不多,那麼,就不會有很多的請求讀到舊值。所以這種情況對業務影響比較小。

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

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

如果業務層要求必須讀取一致性資料,那麼我們就需要在更新資料庫時,現在Redis客戶端暫存併發讀請求,等資料更新完,快取值刪除後,再讀取資料,從而保證一致性。

2.3 藉助訊息佇列刪除

這是對先更新資料庫再刪除快取時,刪除刪除快取失敗的情況的完善,整個流程如下圖

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

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

  • 可以使用阿里的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的解決方案是,對於寫請求,需要配合分散式鎖使用。寫請求進來時,針對同一個資源的修改操作,先加分散式鎖,這樣同一時間只允許一個執行緒去更新資料庫和快取,沒有拿到鎖的執行緒把操作放入到佇列中,延時處理。用這種方式保證多個執行緒操作同一資源的順序性,以此保證一致性。

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