Redis緩存與數據庫數據一致性多方案分析及實踐

方案一

寫流程

:先刪除緩存,刪除之後再更新DB,再異步將數據刷回緩存。如果先更新數據庫再更新緩存,更新數據庫時,程序訪問緩存時還是舊的數據。

讀流程:先讀緩存,如果緩存沒讀到,則去讀DB,之後再異步將數據刷回緩存。

缺點

容災不足

第一步DEL緩存失敗’,如果繼續執行,那麼從’更新完DB’到異步’刷新緩存’緩存期間,數據處於滯後狀態。而且如果緩存處於不可寫狀態,那麼異步刷新那步也可能會失敗,那緩存就會長期處於舊數據。

併發問題

寫寫併發:如果同時有多個服務器的多個線程進行’步驟1.2更新DB’,更新DB完成之後,它們就要進行異步刷緩存,多服務器的異步操作是無法保證順序的,所以後面的刷新操作存在相互覆蓋的併發問題,也就是說,存在先更新的DB操作,反而很晚才去刷新緩存,那這個時候,數據也是錯的

讀寫併發:服務器A在進行’讀操作’,在A服務器剛完成2.2時,服務器B在進行’寫操作’,假設B服務器1.3完成之後,服務器A的1.3才被執行,這個時候就相當於更新前的老數據寫入緩存,最終數據還是錯的。

方案二

方案一 一個比較大的缺陷在於刷新緩存有可能會失敗,而失敗之後緩存中數據就一直會處於錯誤狀態,所以它並不能保證數據的最終一致性”為了保證“數據最終一致性”,我們引入binlog,通過解析binlog來刷新緩存,這樣即使刷新失敗,依然可以進行日誌回放,再次刷新緩存。

寫流程

第一步先刪除緩存,刪除之後再更新DB,我們監聽從庫(資源少的話主庫也ok)的binlog,通過分析binlog我們解析出需要需要刷新的數據,然後讀主庫把最新的數據寫入緩存。

這裡需要提一下:最後刷新前的讀主庫或者讀從庫,甚至不讀庫直接通過binlog解析出需要的數據都是ok的,這由業務決定,比如刷新的數據只是表的一行,那直接通過binlog就完全能解析出來;然而如果需要刷新的數據來自多行,多張表,甚至多個庫的話,那就需要讀主庫或是從庫才行

讀流程

第一步先讀緩存,如果緩存沒讀到,則去讀DB,之後再異步將數據刷回緩存

Redis緩存與數據庫數據一致性多方案分析及實踐

優點:

容災 寫步驟1.4或1.5 如果失敗,可以進行日誌回放,再次重試。無論步驟1.1是否刪除成功,後續的刷新操作是有保證的。

缺點:

只適合簡單業務(每次需要刷新的數據,都來自單表單行),複雜業務容易發生併發問題

為什麼複雜業務就不行呢?我舉個例子
我們假設 一個訂單 = A表信息 + B表信息

Redis緩存與數據庫數據一致性多方案分析及實踐

由於A表先變化,經過1,2,3步後,線程1獲取了A’B (A表是新數據,B表的老數據),當線程1還沒來得及刷新緩存時,併發發生了:

此時,B表發生了更新,經過4,5,6,7將最新的數據A’B’寫入緩存,此時此刻緩存數據是符合要求的。

但是,後來線程1進行了第8步,將A’B寫入數據,使得緩存最終結果 與 DB 不一致。

缺點1的改進

  • 針對單庫多表單次更新的改進:利用事務
Redis緩存與數據庫數據一致性多方案分析及實踐

當AB表的更新發生在一個事務內時,不管線程1、線程2如何讀取,他們都能獲取兩張表的最新數據,所以刷新緩存的數據都是符合要求的。

但是這種方案具有侷限性:那就是隻對單次更新有效,或者說更新頻率低的情況下才適應,比如我們併發的單獨更新C表,併發問題依然會發生。

所以這種方案只針對多表單次更新的情況。

  • 針對多表多次更新的改進:增量更新
Redis緩存與數據庫數據一致性多方案分析及實踐

每張表的更新,在同步緩存時,只獲取該表的字段覆蓋緩存。

這樣,線程1,線程2總能獲取對應表最新的字段,而且Databus對於同表同行會以串行的形式通知下游,所以能保證緩存的最終一致性。

這裡有一點需要提一下:更新“某張表多行記錄“時,這個操作要在一個事務內,不然併發問題依然存在,正如前面分析的

依然是併發問題

即使對於缺點1我們提出了改進方案,雖然它解決了部分問題,但在極端場景下依然存在併發問題。
這個場景,就是緩存中沒有數據的情況:

  • 讀的時候,緩存中的數據已失效,此時又發生了更新
  • 數據更新的時候,緩存中的數據已失效,此時又發生了更新
  • 這個時候,我們在上面提到的“增量更新”就不起作用了,我們需要讀取所有的表來拼湊出初始數據,那這個時候又涉及到讀所有表的操作了,那我們在缺點1中提到的併發問題會再次發生

    適合使用的場景:業務簡單,讀寫QPS比較低的情況。
    這個方案優缺點都比較明顯,binlog用來刷新緩存是一個很棒的選擇,它天然的順序性用來做同步操作很具有優勢;其實它的併發問題來自於Canal 或 Databus。拿Databus來說,由於不同行、表、庫的binlog的消費並不是時間串行的,那怎麼解決這個問題呢。

    方案三

    問題就來自於“讀數據庫” + “寫緩存” 之間的交錯併發,那怎麼來避免呢?
    有一個方法就是:串行化,我們利用MQ將所有“讀數據庫” + “寫緩存”的步驟串行化

    寫流程

    第一步先刪除緩存,刪除之後再更新DB,我們監聽從庫(資源少的話主庫也ok)的binlog,通過分析binlog我們解析出需要需要刷新的數據標識,然後將數據標識寫入MQ,接下來就消費MQ,解析MQ消息來讀庫獲取相應的數據刷新緩存。

    關於MQ串行化,大家可以去了解一下 Kafka partition 機制 ,這裡就不詳述了

    讀流程

    第一步先讀緩存,如果緩存沒讀到,則去讀DB,之後再異步將數據標識寫入MQ(這裡MQ與寫流程的MQ是同一個),接下來就消費MQ,解析MQ消息來讀庫獲取相應的數據刷新緩存。

    Redis緩存與數據庫數據一致性多方案分析及實踐

    優點

    容災完善

    寫流程容災分析

  • 寫1.1 DEL緩存失敗:沒關係,後面會覆蓋
  • 寫1.4 寫MQ失敗:沒關係,Databus或Canal都會重試
  • 消費MQ的:1.5 || 1.6 失敗:沒關係,重新消費即可
  • 讀流程容災分析

    • 讀2.3 異步寫MQ失敗:沒關係,緩存為空,是OK的,下次還讀庫就好了

    無併發問題

    這個方案讓“讀庫 + 刷緩存”的操作串行化,這就不存在老數據覆蓋新數據的併發問題了。

    方案四

    在前一個方案的基礎上實現“強一致性”

    強一致性,包含兩種含義:

    緩存和DB數據一致

    緩存中沒有數據(或者說:不會去讀緩存中的老版本數據)

    首先我們來分析一下,既然已經實現了“最終一致性”,那它和“強一致性”的區別是什麼呢?沒錯,就是“時間差”,所以:

    “最終一致性方案” + “時間差” = “強一致性方案”

    那我們的工作呢,就是加上時間差,實現方式:我們加一個緩存,將近期被修改的數據進行標記鎖定。讀的時候,標記鎖定的數據強行走DB,沒鎖定的數據,先走緩存

    Redis緩存與數據庫數據一致性多方案分析及實踐

    寫流程:

    我們把修改的數據通過Cache_0標記“正在被修改”,如果標記成功,則繼續往下走,後面的步驟與上一篇是一致的《緩存與數據庫一致性系列-03》;那如果標記失敗,則要放棄這次修改。

    何為標記鎖定呢?比如你可以設定一個有效期為10S的key,Key存在即為鎖定。一般來說10S對於後面的同步操作來說基本是夠了~

    如果說,還想更嚴謹一點,怕DB主從延遲太久、MQ延遲太久,或Databus監聽的從庫掛機之類的情況,我們可以考慮增加一個監控定時任務。
    比如我們增加一個時間間隔2S的worker的去對比以下兩個數據:

  • 時間1: 最後修改數據庫的時間
    VS
  • 時間2: 最後由更新引起的’MQ刷新緩存對應數據的實際更新數據庫’的時間
  • 數據1: 可由步驟1.1獲得,並存儲
    數據2: 需要由binlog中解析獲得,需要透傳到MQ,這樣後面就能存儲了
    這裡提一下:如果多庫的情況的話,存儲這兩個key需要與庫一一對應

    如果 時間1 VS 時間2 相差超過5S,那我們就自動把相應的緩存分片讀降級。

    讀流程:

    先讀Cache_0,看看要讀的數據是否被標記,如果被標記,則直接讀主庫;如果沒有被標記,後面的步驟與上一篇是一致的(《緩存與數據庫一致性系列-03》)。

    方案分析

    優點剖析

    1. 容災完善

    我們一步一步來分析:

    寫流程容災分析

  • 寫1.1 標記失敗:沒關係,放棄整個更新操作
  • 寫1.3 DEL緩存失敗:沒關係,後面會覆蓋
  • 寫1.5 寫MQ失敗:沒關係,Databus或Canal都會重試
  • 消費MQ的:1.6 || 1.7 失敗:沒關係,重新消費即可
  • 讀流程容災分析

  • 讀2.1 讀Cache_0失敗:沒關係,直接讀主庫
  • 讀2.3 異步寫MQ失敗:沒關係,緩存為空,是OK的,下次還讀庫就好了
  • 2. 無併發問題

    這個方案讓“讀庫 + 刷緩存”的操作串行化,這就不存在老數據覆蓋新數據的併發問題了

    缺點剖析

    1. 增加Cache_0強依賴

    這個其實有點沒辦法,你要強一致性,必然要犧牲一些的。
    但是呢,你這個可以吧Cache_0設計成多機器多分片,這樣的話,即使部分分片掛了,也只有小部分流量透過Cache直接打到DB上,這是完全是可接受的

    2. 複雜度是比較高的

    涉及到Databus、MQ、定時任務等等組件,實現起來複雜度還是有的


    專注於技術熱點大數據,人工智能,JAVA、Python、 C 、GO、Javascript等語言最新前言技術,及業務痛點問題分析,請關注【編程我最懂】共同交流學習。


    分享到:


    相關文章: