More  

小編的世界 優質文選 資料

數據庫與Redis緩存一致性


2021年5月28日 - 資料小編 DDDInJava 
   

DDDInJava

如今,Redis已成為最瀏覽的緩存解決方案之一,盡管關系型數據庫帶了許多很棒的功能,如ACID。但是,為了使用這些功能,數據庫的性能在高負載的情況下也會有所下降。

為了解決這個問題,許多公司和網站在應用層和數據訪問層之間都會增加一個緩存層。通常使用內存中緩存來實現這個緩存層。正如我們所知,傳統的關系型數據庫的性能瓶頸通常是存儲I/O。由於科技的發展和進步,主存儲器的價格一直在下降,增加內存已經不是什麼難事了,因此現在可以在內存中緩存一部分熱點數據來提供性能。

背景

雖然我們可以把熱點數據存儲在內存中,但是這種方法已經會讓人頭疼,因為我們失去了對數據單一源的控制,相同的數據存儲在數據庫和內存中。如何避免阻塞情況下確保Redis中的數據和數據庫中的數據一致?

下面,我們將了解一些常用的解決方案,這些方案大部分情況下幾乎是正確的,因為它們可以保證99.9%的情況下Reids和數據庫中的數據一致。但是高並發情況下就可能出現髒數據。

緩存過期

通常我們常用的方案可能就是緩存過期,但是不得不承認這是保證一致性的糟糕方案。

例如:我們設置緩存過期時間為30分鐘,你就要確保這30分鐘內不會讀取到髒數據。如果將過期時間設置得更短會不會好點?

如果你的網站具有巨大的流量和高並發服務,這樣你確實縮短了不一致的可能,但是已經違背了使用緩存的初衷,可能會有很多緩存違背命中。

暫存

暫存模式通常是這樣的:

對於不可變的操作(讀取):

緩存命中:直接從Redis中返回數據,無需查詢MySql

緩存未命中:查詢MySql(可以使用只讀來提高性能),將返回的數據放到Redis中,然後返回結果給客戶端

對於可變操作(創建、更新、刪除):

創建、更新、刪除MySql中的數據

刪除Redis中的數據,總是刪除而不是更新緩存,下一個緩存未命中將插入新值

這種方法通常被我們使用,實際上,它是MySql和Redis之間實現緩存一致性的標准。但是,這種方法也存在一些問題:

正常情況下,假設寫入MySql / Redis絕對不會失敗,它通常可以保證最終一致性。假設我們有個熱門服務,做了負載分別放在A、B兩個服務器上,在某個時刻A已經成功更新了MySql中的數據。在刪除Reids中的數據之前,B嘗試讀取這個數據,然後B將命中緩存,因為辭職A還沒有來得及刪除Redis中的數據。因此B就讀取到了髒數據,但是Redis中的數據最終還是會被刪除,其他服務最終將讀取到更新後的數據。

在極端情況下,它也不能保證最終的一致性。同樣的情況,如果A在嘗試刪除Redis中的數據前剛好被kill掉,這樣Redis中的數據將無法被刪除。這樣其他服務器都會讀取到髒數據。

即使在正常情況下,也存在極低的可能性,最終一致性得不到保障。假設A嘗試讀取數據,緩存未命中,然後從MySql中讀取數據。此時,由於高並發和巨大的流量導致A的服務器突然卡了。這是B嘗試更新相同的數據,D更新MySql, 並刪除了Redis中的數據。之後A恢複並將其查詢結果保存到了Redis。這樣後面其他服務都會讀出到髒數據,雖然這種可能性非常低。

暫存 - 變體1

暫存模式 - 變體1為:

對於不可變操作(讀取):

緩存命中:直接從Redis中返回數據,無需查詢MySql

緩存未命中:查詢MySql(可以使用只讀來提高性能),將返回的數據放到Redis中,然後返回結果給客戶端

對於可變的操作(創建、更新、刪除):

刪除Redis中的數據

創建、更新、刪除MySql數據

這種方案也是非常糟糕的。假設A嘗試更新數據,在某個時刻,A已經成功刪除了Reids中的數據。在A更新MySql中的數據之前,B嘗試讀取相同的數據,且緩存未命中。然後B查詢MySql並將數據保存到Redis中。注意,此時MySql中的數據尚未更新。由於A後面不會再刪除Redis中的數據,因此舊的數據依舊保存在Redis中了。

暫存 - 變體2

暫存模式 - 變體1為:

對於不可變操作(讀取):

緩存命中:直接從Redis中返回數據,無需查詢MySql

緩存未命中:查詢MySql(可以使用只讀來提高性能),將返回的數據放到Redis中,然後返回結果給客戶端

對於可變的操作(創建、更新、刪除):

創建、更新、刪除MySql數據

在Redis中創建、更新、刪除數據

這也是一個不好的解決方案。假設,A、B都試圖更新數據,A在B之前更新了MySql。但是B會在A之前更新Redis。最終,Mysq中的數據會由B更新。但是Redis中的數據則由A更新,這將導致不一致。

通讀

通讀模式為:

對於不可變的操作(讀取):

客戶端始終從Redis中讀取。緩存未命中,這Redis應具有自動從數據庫中讀取的功能。

對於可變的操作(創建、更新、刪除):

此策略不處理可變操作。它與只寫模式結合使用

直寫

只寫模式為:

對於不可變的操作(讀取):

此策略不處理不變的操作。它與通讀模式結合使用

對於可變的操作(創建、更新、刪除):

客戶端僅在Redis中創建、更新、刪除數據。Redis必須原子地把數據同步到MySql

直接模式的缺點非常明顯,首先大部分的緩存中間件並不支持此功能。其次,Redis是緩存而不是RDBMS。Redis的主要目的並不是彈性擴展,因此在更改複制到MySql之前,很可能會丟失。即使Reids現在支持RDB和AOF之類的持久化技術,但是仍然不建議使用。

後寫

對於不可變的操作(讀取):

此策略不處理不變的操作。它與通讀模式結合使用

對於可變的操作(創建、更新、刪除):

客戶端需要在Redis中創建、更新、刪除數據。Redis將更改放到消息隊列中,然後返回成功給客戶端。更改被異步複制到MySql中。

後寫模式與直接模式不同的時,它異步地將更改複制到MySql,這樣客戶端就不必等待。所以提供了吞吐量。Redis從5.0開始支持Redis流,這可能是一個不錯的方式,為了提供性能,可以合並更改並批量更新到MySql中。後寫模式的缺點同樣也是許多緩存中間件並不支持此功能。其次,使用消息隊列必須是FIFO,保證最終結果。而且還要保證消息隊列的並發等,如MQ。

雙刪

雙刪模式為:

對於不可變的操作(讀取):

緩存命中:直接從Redis中返回數據,無需查詢MySql

緩存未命中:查詢MySql(可以使用只讀來提高性能),將返回的數據放到Redis中,然後返回結果給客戶端

對於可變的操作(創建、更新、刪除):

刪除Redis中的數據

創建、更新、刪除MySql中的數據

sleep一段時間如500ms

再次刪除Redis中的數據

這種模式目前是最被接受的方案。它結合了暫存 - 變體1,由於它是基於暫存 - 變體1改進的,因為在大多數情況下它可以保證最終一致性。它也通過sleep來確保刪除了髒數據。盡管仍然存在極端的情況會破壞最終一致性,但是這個可能性很小。

後寫 - 變體1

來自阿裏巴巴canal項目的一種新穎模式,它是通過另外一種方式執行複制。它沒有直接把Redis中的更改複制到MySql,而是通過MySql的binlog將數據複制到Redis。與後寫模式相比,這更好地保證了持久性和一致性。由於binlog是RDMS技術的一部分,因此它非常具有彈性,而且這種技術,早就被用在MySql之間做主從同步。

結論

對於實際情況而言,99.9%的正確性已經足夠了,我們應該謹記使用Redis的最初目的。

  大家在看