天天看點

如何保證資料庫和緩存資料一緻性

作者:奇思妙想002

政策一:先更資料庫,再更緩存

場景說明:服務A和服務B同時對資料進行更新。當服務A更新完DB,服務B更新完DB,然後服務B更新緩存資料,最後服務A更新緩存資料就會導緻DB和緩存資料不一緻。

解決方法:先更新資料庫在删除緩存,讓讀請求從資料庫加載最新資料到緩存。

删除緩存而不是更新緩存的優點:

1.如果寫入Cache的值,是經過複雜計算才得到的話。更新緩存頻率高的話,就會大大降低性能;

2.寫多讀少的情況下實時更新緩存比較浪費緩存空間且降低的性能。

政策二:先删緩存,再更新資料庫

場景說明:寫操作是先删Cache(操作1)再寫DB(操作2),如果在此期間發生并發讀,讀取的動作很容易發生操作1、操作2的中間,進而讀取到過時的資料,最終導緻Cache和DB不一緻。更為嚴重的時候,讀操作把過期資料刷入Cache後,會導緻後面比較長時間的不一緻。這個時間,一直持續到緩存過期,如說4個小時(以項目中的配置時間為準)。

  • step 1:微服務A去執行delete Cache
  • step 2:微服務B去執行load from DB
  • step 3:微服務B去執行update Cache
  • step 4:微服務A去執行update DB

政策三:先更資料庫,再删緩存

場景說明:按照先更資料庫,再删緩存的政策,則微服務Provider執行個體A、B可能會出現下面的執行次序:

  • step 1:微服務A去執行update DB
  • step 2:微服務B去執行load from Cache
  • step 3:微服務A去執行delete Cache,但是發生了延遲

但是等到寫操作删除Cache(操作2)的動作執行完成之後,Cache和DB的資料,會恢複一緻性。無論如何,政策三(先寫DB再删Cache),比政策二(先删Cache再寫DB)發生資料不一緻的時間短。相比較而言,推薦大家使用政策三,而不是政策二。

本政策依然會存在一下問題:

1.寫DB(操作1)和删Cache(操作2)之間,存在短時間的資料不一緻

2.如果删Cache失敗,存在較長時間的資料不一緻,這個時間會一直持續到Cache過期

政策四:延遲雙删政策

延遲雙删是基于政策二進行改進,就是先删Cache,後寫DB,最後延遲一定時間,再次删Cache。

場景說明:微服務Provider執行個體A先删Cache(操作1),再寫DB(操作2),最後再二次延遲删除Cache(操作3)。在操作2之前,如果發生并發讀,從DB讀取到過時資料,可能出現DB和Cache資料不一緻問題。不過存在不一緻的時間不會太長,因為還有延遲删除緩存的操作來保證最終資料庫和緩存的資料一緻性。

本政策依然會存在一下問題:

1.如果寫太頻繁會影響緩存的性能;

2.極端情況導緻第二次删除緩存失敗将會退回到政策二,會很長一段時間資料不一緻。這個時間會一直持續到Cache過期。

政策五:先更資料庫,再基于隊列删緩存

政策六是基于政策三進行改進,操作次序還是先寫DB然後删除緩存。不同的是,政策六引入隊列,把删Cache的操作加入隊列,背景會有一個異步線程、或者程序去異步消費隊列中的删除任務,去執行删Cache的操作。

基于隊列删緩存,可以細分為:

  • 第1種細分的方案:基于記憶體隊列删除緩存
  • 第2種細分的方案:基于消息隊列删除緩存
  • 第3種細分的方案:基于binlog+消息隊列删除緩存

基于記憶體隊列删除緩存

此政策把删Cache的操作加入任務隊列,背景會有一個異步線程去異步消費任務隊列裡面的删除任務,去執行删Cache的操作,如果緩存删除失敗,可以重試多次,確定删除成功。

場景說明:微服務Provider執行個體A先寫DB(操作1),再将删Cache操作加入任務隊列(操作2)。在删除Cache操作真正執行完成之前,其他的資料讀取操作,都會讀取Cache中的過期資料,出現DB和Cache資料不一緻問題。但是這種不一緻,是短暫的。任務隊列的消費線程,會異步執行删除Cache的任務,并且會不斷重試確定成功,删除Cache之後,DB和Cache資料不一緻問題就會得到解決。

與政策四相比,政策六的優勢是:

(1)在寫操作比較頻繁的場景,政策四有兩次删Cache操作,可能會對Redis造成一定的壓力;政策六隻有一次删Cache操作,Redis壓力小一半。

(2)政策四如果删Cache失敗,沒有引入重試政策;政策六會多次重試,確定删Cache成功,如果重試多次仍然不成功,可以執行運維預警。

(3)政策四将寫DB、删Cache這兩個操作耦合在了一起,沒有很好的做到單一職責;政策六将寫DB、删Cache兩個操作解耦,子產品職責更加單一。

那麼,政策六的問題是啥呢?

(1)如果寫操作非常頻繁,隊列的任務比較多,可能消費會比較慢;需要引入多線程機制,加快消費速度。

(2)程式複雜度成倍上升,引入消費線程、任務隊列,并且還需要不斷進行性能優化。

(3)記憶體隊列是JVM程序的内部隊列,如果JVM崩潰,記憶體隊列沒有來得及處理的Cache記錄删除任務會丢失,這些資料的Cache記錄和DB記錄會長時間不一緻

基于消息隊列删除緩存

将删除Cache的任務儲存在記憶體隊列,并不是高可靠的。引入的RocketMq消息隊列是高可用的類型消息隊列,不是單節點的類型消息隊列,進而保障消息記錄的高可用,保障Cache的删除操作隻要沒有被執行成功,就不會丢失。

引入高可用RocketMq消息隊列之後,執行雙寫操作的Provider A的操作流程,有小幅度的調整。Provider A需要将删除Cache的操作,序列化成Rocketmq消息,然後寫入高可用Rocketmq消息隊列中間件即可。然後,由專門的消費者(Cache Delete Consumer)進行消息的消費,根據消息内容執行Cache記錄删除工作。

基于消息隊列方案的優勢是:增加了Cache删除的可靠性,避免了因JVM崩潰所導緻的記憶體隊列中的記錄丢失的問題。

基于binlog+消息隊列删除緩存

以Mysql為例,可以使用阿裡的Canal中間件,采集在資料寫入Mysql時生成的binlog日志,然後将日志發送到RocketMq隊列。在消費端,可以編寫一個專門的消費者(Cache Delete Consumer)完成緩存binlog日志訂閱,篩選出其中的更新類型log,解析之後進行對應Cache的删除操作,并且通過RocketMq隊列ACK機制确認處理這條更新log,保證Cache删除能夠得到最終的删除。

本方案的優勢是:

微服務Provider在執行DB和Cache雙寫時,隻需要執行寫入DB的操作就可以了,大大簡化了微服務Provider的業務邏輯。Cache的删除工作已經完全被Canal、RocketMq、專門的消費者(Cache Delete Consumer)三者互相結合去接管了。

說在最後:

1.使用緩存是為了獲得高性能、高吞吐那麼就勢必要犧牲資料的強一緻性。但是我們可以通過不同的方案來盡量減少不一緻的情況或不一緻的時長,保證最終一緻。

2.緩存一定要設定過期時間。太長太短都不好,太短就失去了緩存的意義。太長有可能會導緻緩存和DB資料在較長時間内的不一緻。

3.為啥DB和Cache沒有辦法強一緻呢?主要是寫DB和删Cache是兩個獨立的操作,兩個操作并沒有保證原子性。如果一定要強CP,就需要非常複雜的低性能方案,有點得不償失。

繼續閱讀