如何保證緩存(redis)與數據庫(MySQL)的一致性

【簡介】
  對於熱點數據(經常被查詢,但不經常被修改的數據),我們可以將其放入redis緩存中,以增加查詢效率,但需要保證從redis中讀取的數據與數據庫中存儲的數據最終是一致的。本文基於“孤獨煙”與“58沈劍”兩位的文章,針對一致性的問題進行了彙總總結,兩位的原文鏈接見文末。

【前言】

  客戶端對數據庫中的數據主要有兩類操作,讀(select)與寫(DML)。針對放入redis中緩存的熱點數據,當客戶端想讀取的數據在緩存中就直接返回數據,即命中緩存(cache hit),當讀取的數據不在緩存內,就需要從數據庫中將數據讀入緩存,即未命中緩存(cache miss)。所以讀操作並不會導致緩存與數據庫中的數據不一致。
  對於寫操作(DML),緩存與數據庫中的內容都需要被修改,但兩者的執行必定存在一個先後順序,這可能會導致緩衝與數據庫中的數據不再一致,此時主要需要考慮兩個問題:
  1、執行順序的問題:先更新緩存還是先更新數據庫?
  2、更新緩存的策略問題:當緩存中的內容變化時,是選擇修改緩存(update),還是直接淘汰緩存(delete)?

針對這兩點問題,一共可以分爲四種方案:
  1、先更新緩存,再更新數據庫;
  2、先更新數據庫,再更新緩存;
  3、先淘汰緩存,再更新數據庫;
  4、先更新數據庫,再淘汰緩存。

【疑問一】更新cache還是淘汰cache?

  我們先來討論緩存更新的策略問題:即更新緩存時,是直接淘汰cache中的舊數據,還是將更新操作也放在緩存中進行?

淘汰cache:
優點:操作簡單,無論更新操作是否複雜,直接將緩存中的舊值淘汰
缺點:淘汰cache後,下一次查詢無法在cache中查到,會有一次cache miss,這時需要重新讀取數據庫
更新cache:
  更新chache的意思就是將更新操作也放到緩衝中執行,並不是數據庫中的值更新後再將最新值傳到緩存
優點:命中率高,直接更新緩存,不會有cache miss的情況
缺點:更新cache消耗較大
  當更新操作簡單,如只是將這個值直接修改爲某個值時,更新cache與淘汰cache的消耗差不多
  但當更新操作的邏輯較複雜時,需要涉及到其它數據,如用戶購買商品付款時,需要考慮打折等因素,這樣需要緩存與數據庫進行多次交互,將打折等信息傳入緩存,再與緩存中的其它值進行計算才能得到最終結果,此時更新cache的消耗要大於直接淘汰cache
所以選擇直接淘汰緩存更好,如果之後需要再次讀取這個數據,最多會有一次緩存失敗

【更新cache的另一個問題】
  我們現在已經知道直接淘汰cache比更新cache要更好,現在再進一步思考下更新cache的其它問題。
  對於上文列舉的四種方案的前兩種,即:
    1、先更新(update)緩存,再更新數據庫;
    2、先更新數據庫,再更新(update)緩存;
  當併發較大,同時有兩個線程需要對同一個數據進行更新時,可能會出現以下問題:
方案一、先更新(update)緩存,再更新數據庫
  線程A更新了緩存
  線程B更新了緩存
  線程B更新了數據庫
  線程A更新了數據庫
方案二、先更新數據庫,再更新(update)緩存
  線程A更新了數據庫
  線程B更新了數據庫
  線程B更新了緩存
  線程A更新了緩存
如果不同的線程對同一個數據進行更新時,更新的先後順序有明確要求,那麼上述兩種方案都會導致數據的不一致
解決的思路是“串行化”,即對同一個數據的修改,要以串行化的方式先後執行

結論:更新cache的消耗更大,且很有可能造成數據的不一致,所以推薦直接淘汰cache

【疑問二】執行順序的問題

  究竟是先淘汰緩存還是先更新數據庫?
這裏主要分爲兩個方面來考慮:
  1、更新數據庫與淘汰緩存是兩個步驟,只能先後執行,如果在執行過程中後一步執行失敗,哪種方案的影響最小?
  2、如果不考慮執行失敗的情況,但更新數據庫與淘汰緩存必然存在一個先後順序,在上一個操作執行完畢,下一個操作還未完成時,如果併發較大,仍舊會導致數據庫與緩存中的數據不一致,在這種情況下,用哪種方案影響最小?

另外,對於數據庫而言,讀寫操作可以只作用在同一臺服務器上,即底層只有一個數據庫,也可以將讀操作放在從庫,寫操作放在主庫,即底層是主從架構,對於主從架構還需要考慮主從延遲,本文針對的是單節點模式。

【數據庫是單節點】

情景一:更新數據庫與淘汰緩存需要先後執行,如果在執行過程中後一步執行失敗,哪種方案對業務的影響最小?
  方案一、先淘汰緩存,再更新數據庫
如果第一步淘汰緩存成功,第二步更新數據庫失敗,此時再次查詢緩存,最多會有一次cache miss
  方案二、先更新數據庫,再淘汰緩存
如果第一步更新數據庫成功,第二部淘汰緩存失敗,則會出現數據庫中是新數據,緩存中是舊數據,即數據不一致
解決辦法:爲確保緩存刪除成功,需要用到“重試機制”,即當刪除緩存失效後,返回一個錯誤,由業務代碼再次重試,直到緩存被刪除。

但對於方案一,如果更新數據庫失敗其實也是一個問題,爲了確保數據庫中的數據被正常更新,也需要“重試機制”,即當數據庫中的數據更新失敗後,也需要人工或業務代碼再次重試,直到更新成功。

【結論】總體而言,雖然方案二導致數據不一致的可能性更大,但在業務中,無論是淘汰緩存還是更新數據庫,我們都需要確保它們真正完成了,所以個人認爲在情景一下兩種方案並沒有什麼優劣之分。

重試機制的原理圖:

情景二:假設沒有操作會執行失敗,但執行前一個操作後無法立即完成下一個操作,在併發較大的情況下,可能會導致數據不一致。此時,哪種方案對業務的影響最小?

方案一、先淘汰緩存,再更新數據庫

1、在正常情況下,A、B兩個線程先後對同一個數據進行讀寫操作:
  A線程進行寫操作,先淘汰緩存,再更新數據庫
  B線程進行讀操作,發現緩存中沒有想要的數據,從數據庫中讀取更新後的新數據
此時沒有問題
2、在併發量較大的情況下,採用同步更新緩存的策略:
  A線程進行寫操作,先成功淘汰緩存,但由於網絡或其它原因,還未更新數據庫或正在更新
  B線程進行讀操作,發現緩存中沒有想要的數據,從數據庫中讀取數據,但此時A線程還未完成更新操作,所以讀取到的是舊數據,並且B線程將舊數據放入緩存。注意此時是沒有問題的,因爲數據庫中的數據還未完成更新,所以數據庫與緩存此時存儲的都是舊值,數據沒有不一致
  在B線程將舊數據讀入緩存後,A線程終於將數據更新完成,此時是有問題的,數據庫中是更新後的新數據,緩存中是更新前的舊數據,數據不一致。如果在緩存中沒有對該值設置過期時間,舊數據將一直保存在緩存中,數據將一直不一致,直到之後再次對該值進行修改時纔會在緩存中淘汰該值
此時可能會導致cache與數據庫的數據一直或很長時間不一致

3、在併發量較大的情況下,採用異步更新緩存的策略:
  A線程進行寫操作,先成功淘汰緩存,但由於網絡或其它原因,還未更新數據庫或正在更新
  B線程進行讀操作,發現緩存中沒有想要的數據,從數據庫中讀取數據,但B線程只是從數據庫中讀取想要的數據,並不將這個數據放入緩存中,所以並不會導致緩存與數據庫的不一致
  A線程更新數據庫後,通過訂閱binlog來異步更新緩存
此時數據庫與緩存的內容將一直都是一致的

進一步分析:
如果採取同步更新緩存的策略,即如果緩存中沒有數據,就讀取數據庫並將數據直接放入緩存,可能會導致數據長時間的不一致
在這種情況下,可以用一些方法來進行優化:
1、用串行化的思路
  即保證對同一個數據的讀寫嚴格按照先後順序串行化進行,避免併發較大的情況下,多個線程同時對同一數據進行操作時帶來的數據不一致性。
  關於如何用串行化保證一致性,詳見“58沈劍”的文章“緩存與數據庫一致性保證”,原文鏈接見文末。
2、延時雙刪+設置緩存的超時時間
  不一致的原因是,在淘汰緩存之後,舊數據再次被讀入緩存,且之後沒有淘汰策略,所以解決思路就是,在舊數據再次讀入緩存後,再次淘汰緩存,即淘汰緩存兩次(延遲雙刪)
引入延時雙刪後,執行步驟變爲下面這種情形:
  A線程進行寫操作,先成功淘汰緩存,但由於網絡或其它原因,還未更新數據庫或正在更新
  B線程進行讀操作,從數據庫中讀入舊數據,共耗時N秒
  在B線程將舊數據讀入緩存後,A線程將數據更新完成,此時數據不一致
  A線程將數據庫更新完成後,休眠M秒(M比N稍大即可),然後再次淘汰緩存,此時緩存中即使有舊數據也會被淘汰,此時可以保證數據的一致性
  其它線程進行讀操作時,緩存中無數據,從數據庫中讀取的是更新後的新數據

利用延遲雙刪,可以很好的解決數據不一致的問題,其中A線程休眠的M秒,需要根據業務上讀取的時間來衡量,只要比正常讀取消耗的實際稍大就可以。但是個人感覺實際業務中需要根據場景來設置休眠的時間,這個不好確定。

引入延時雙刪後,存在兩個新問題:
  1、A線程需要在更新數據庫後,還要休眠M秒再次淘汰緩存,等所有操作都執行完,這一個更新操作才真正完成,降低了更新操作的吞吐量
解決辦法:用“異步淘汰”的策略,將休眠M秒以及二次淘汰放在另一個線程中,A線程在更新完數據庫後,可以直接返回成功而不用等待。
  2、如果第二次緩存淘汰失敗,則不一致依舊會存在
解決辦法:用“重試機制”,即當二次淘汰失敗後,報錯並繼續重試,直到執行成功個人

“異步淘汰”策略:
_2
A線程執行完步驟2不再休眠Ms,而是往消息總線esb發送一個消息,發送完成之後馬上就能返回

【小結】
在單節點下,用“先刪緩存,再更新”的策略,如果採用同步更新緩存的策略,可能會導致數據長時間的不一致,可以通過一些方法來儘量避免不一致;如果採用異步更新緩存的策略,就不會導致數據不一致

方案二、先更新數據庫,再淘汰緩存

在正常情況下:
  A線程進行寫操作,更新數據庫,淘汰緩存
  B線程進行讀操作,從數據庫中讀取新的數據
不會有問題

在併發較大的情況下,情形1:
  A線程進行寫操作,更新數據庫,還未淘汰緩存
  B線程從緩存中可以讀取到舊數據,此時數據不一致
  A線程完成淘汰緩存操作
  其它線程進行讀操作,從數據庫中讀入最新數據,此時數據一致
不過這種情況並沒有什麼大問題,因爲數據不一致的時間很短,數據最終是一致的

在併發較大的情況下,情形2:
  A線程進行寫操作,更新數據庫,但更新較慢,緩存也未淘汰
  B線程進行讀操作,讀取了緩存中的舊數據
但這種情況沒什麼問題,畢竟更新操作都還未完成,數據庫與緩存中都是舊數據,沒有數據不一致

在併發較大的情況下,情形3:
  A線程進行讀操作,緩存中沒有相應的數據,將從數據庫中讀數據到緩存,
此時分爲兩種情況,還未讀取數據庫的數據,已讀取數據庫的數據,不過由於網絡等問題數據還未傳輸到緩存
  B線程執行寫操作,更新數據庫,淘汰緩存
  B線程寫操作完成後,A線程纔將數據庫的數據讀入緩存,對於第一種情況,A線程讀取的是B線程修改後的新數據,沒有問題,對於第二種情況,A線程讀取的是舊數據,此時數據會不一致
不過這種情況發生的概率極低,因爲一般讀操作要比寫操作要更快
萬一擔心存在這種可能,可以用“延遲雙刪”策略,在A線程讀操作完成後再淘汰一次緩存

【小結】
在該方案下,無論是採用同步更新緩存(從數據庫讀取的數據直接放入緩存中),還是異步更新緩存(數據庫中的數據更新完成後,再將數據同步到緩存中),都不會導致數據的不一致
該方案主要只需要擔心一個問題:如果第二步淘汰緩存失敗,則數據會不一致
解決辦法之前也提到過,用“重試機制”就可以,如果淘汰緩存失敗就報錯,然後重試直到成功

【單節點下兩種方案對比】
先淘汰cache,再更新數據庫:
  採用同步更新緩存的策略,可能會導致數據長時間不一致,如果用延遲雙刪來優化,還需要考慮究竟需要延時多長時間的問題——讀的效率較高,但數據的一致性需要靠其它手段來保證
  採用異步更新緩存的策略,不會導致數據不一致,但在數據庫更新完成之前,都需要到數據庫層面去讀取數據,讀的效率不太好——保證了數據的一致性,適用於對一致性要求高的業務
先更新數據庫,再淘汰cache:
  無論是同步/異步更新緩存,都不會導致數據的最終不一致,在更新數據庫期間,cache中的舊數據會被讀取,可能會有一段時間的數據不一致,但讀的效率很好——保證了數據讀取的效率,如果業務對一致性要求不是很高,這種方案最合適

【其它】
重試機制可以採利用“消息隊列MQ”來實現
通過訂閱binlog來異步更新緩存,可以通過canal中間件來實現

原文鏈接:
【58沈劍原文鏈接】
緩存架構設計細節二三事2016-03-08
緩存與數據庫一致性保證2016-03-16
主從DB與cache一致性 2016-03-24
緩存,究竟是淘汰,還是修改?2018-07-02
究竟先操作緩存,還是數據庫? 2018-07-09
Cache Aside Pattern 2018-07-11
緩存與數據庫不一致,咋辦? 2018-07-12

【孤獨煙原文鏈接】
分佈式之數據庫和緩存雙寫一致性方案解析 2018-05-15
分佈式之數據庫和緩存雙寫一致性方案解析(二) 2018-06-28
分佈式之數據庫和緩存雙寫一致性方案解析(三)2018-07-13

文章來源:https://developer.aliyun.com/article/712285

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章