緩存與數據庫雙寫一致最佳解決方案分析

寫在最前面

在大型互聯網應用當中如果你的應用引入了緩存機制,那麼有一個大前提就是你的業務場景上必須得接受數據的新鮮度上有可能會有一定時間的延遲。
刪除緩存失敗是一個極小概率事件,且在不能保證所有操作100%成功的機率下,採用JOB補償的機制是目前比較成熟的解決方案。
大併發量寫請求的應用,不可能去實時寫DB,基本都採用隊列+消息異步寫DB的機制,不然會有大量的併發問題

緩存機制介紹

如今利用緩存機制來提高查詢效率已被廣泛用在各大生產環境,查詢數據的一般流程如下所示

  1. 如果數據在緩存裏邊有,則直接從緩存取數據返回。
  2. 如果緩存中沒有想要的數據,則先去查詢數據庫,然後將數據庫查出來的數據寫到緩存中再返回

沒有更新數據的情況下,數據庫和緩存的數據是保持一致的;但是當要執行數據的更新操作的時候,數據庫和緩存的數據就會出現不一致的情況

因此,爲了解決數據不一致的問題,需要在更新數據庫的時候,對緩存做一些額外的操作,有以下幾種方案

  1. 先刪緩存,再更新數據庫
  2. 先更新數據庫,再刪緩存
  3. 緩存延時雙刪,更新前先刪除緩存,然後更新數據,再延時刪除緩存
  4. 監聽MySQL binlog進行緩存更新

之所以緩存不採取更新操作而是直接刪除是因爲:高併發環境下,無論是先操作數據庫還是後操作數據庫而言,如果加上更新緩存,那就更加容易導致數據庫與緩存數據不一致問題。(刪除緩存直接且簡單很多)

先刪除緩存,再更新數據庫

該方案在線程A進行數據更新操作,線程B進行查詢操作時,有可能出現下面的情況導致數據不一致:

  1. 線程A刪除緩存
  2. 線程B查詢數據,發現緩存數據不存在
  3. 線程B查詢數據庫,得到舊值,寫入緩存
  4. 線程A將新值更新到數據庫

這樣一來,緩存中的數據仍然是舊值

如果線程B執行的是更新操作,線程B查詢得到的是舊值,A更新到數據庫新值,然後B基於舊值計算寫入了計算後的值,A的更新操作被抹去了,這種情況下屬於更新數據事務原子性問題,需要用分佈式鎖來解決。

先更新數據庫,再刪除緩存

當緩存失效時,線程B原子性被破壞時會出現不一致問題:

  1. 緩存失效了
  2. 線程B從數據庫讀取舊值
  3. 線程A從數據庫讀取舊值
  4. 線程B將新值更新到數據庫
  5. 線程B刪除緩存
  6. 線程A將舊值寫入緩存

這種情況概率很低,實際上數據庫的寫操作會比讀操作慢得多,即線程B會比線程A慢,而讀操作必需在寫操作前進入數據庫操作,而又要晚於寫操作更新緩存這種情況下只需要線程B延時刪除緩存就好

另外在數據庫主從同步的情況下,延時刪除還能防止數據更新還未從主數據庫同步到從數據庫的情況。

延時雙刪

延時雙刪即先刪除緩存,然後更新數據,再延時n ms後刪除緩存,這個我認爲作用和更新數據庫再刪除緩存的策略幾乎是等同的(歡迎討論)

之所以設計爲延時雙刪的目的在於當最後一次延時刪除緩存失敗的情況發生,至少一致性策略只會退化成先刪緩存再更新數據的策略。

刪除緩存失敗這種事情個人認爲在生產環境緩存高可用的情況下幾乎不會出現,且這種情況如果發生了,不如考慮一下重試機制。

異步更新緩存(基於訂閱binlog的同步機制)

通過異步更新緩存將緩存與數據庫的一致性同步從業務中獨立出來統一處理,保證數據一致性

整體技術思路:

  1. 讀Redis:熱數據基本都在Redis
  2. 寫MySQL:增刪改都是操作MySQL
  3. 更新Redis數據:訂閱MySQ的數據操作記錄binlog,來更新到Redis

數據操作分爲兩大部分:

  • 全量更新(將全部數據一次性寫入redis)
  • 增量更新(實時更新)

這樣一旦MySQL中產生了新的寫入、更新、刪除等操作,就可以把binlog相關的消息通過消息隊列推送至Redis,Redis再根據binlog中的記錄,對Redis進行更新。

這種同步機制類似於MySQL的主從備份機制,可以結合使用阿里的canal對MySQL的binlog進行訂閱。

總結

綜上所述,異步更新緩存、更新後延遲刪與延遲雙刪都是不錯的一致性解決方案,但除了異步更新緩存,其餘兩個方案都會對業務線的代碼有侵入性。


參考:
鏈接:https://www.jianshu.com/p/dc1e5091a0d8
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章