Redis 數據一致性

概述

當我們在使用緩存時,如果發生數據變更,那麼你需要同時操作緩存和數據庫,而它們兩個又分屬不同的系統,因此無法做到同時操作成功或失敗,因此在併發讀寫下很可能出現緩存與數據庫數據不一致的情況

理論上可以通過分佈式事務保證同時操作成功或失敗,但這會影響系統性能,一般很少使用。雖然沒辦法做到緩存和數據庫強一致,但我們可以讓他們的數據儘可能在絕大部分時間內保持一致,並保證最終是一致的


緩存更新設計

一般來說都是採用刪除緩存的方式更新緩存,這就涉及到先刪除緩存還是先更新數據庫的順序問題了

1. 先刪除緩存,後更新數據庫

先刪除緩存,後更新數據庫,如果數據庫沒有更新成功,下次讀緩存發現不存在,則從數據庫讀取,並重建緩存,此時數據庫和緩存依舊保持一致,但還是舊值

高併發下,假設有兩個線程併發讀寫數據,可能會發生以下場景:

  • 線程 A 要更新 X = 2(原值 X = 1)
  • 線程 A 先刪除緩存
  • 線程 B 讀緩存,發現不存在,從數據庫中讀取到舊值(X = 1)
  • 線程 A 將新值寫入數據庫(X = 2)
  • 線程 B 將舊值寫入緩存(X = 1)
  • 最終 X 的值在緩存中是 1(舊值),在數據庫中是 2(新值),發生不一致

可見,在高併發下這種方式容易出現長時間的髒數據,一般不建議使用

2. 先更新數據庫,後刪除緩存

先更新數據庫,後刪除緩存,如果緩存沒有刪除成功,數據庫是最新值,緩存中是舊值,會發生不一致

再看兩個線程併發讀寫數據:

  • 某一時刻緩存中 X 失效不存在(數據庫 X = 1)
  • 線程 A 讀取數據庫,得到舊值(X = 1)
  • 線程 B 更新數據庫(X = 2)
  • 線程 B 刪除緩存
  • 線程 A 將舊值寫入緩存(X = 1)
  • 最終 X 的值在緩存中是 1(舊值),在數據庫中是 2(新值),發生不一致

這種方式依舊會出現數據不一致,但概率很低,所以普遍採用這種方式


更多優化

通過前面分析,我們採用了先更新數據庫,再刪除緩存的方式,還可以進一步優化

1. 保證兩步都執行成功

前面提到,無論採用哪種方式,只要第二步失敗都會有問題,所以我們需要保證第二步成功執行

一種簡單的辦法是失敗就重試,但這會佔用資源,並且立即重試大概率還是失敗,所以可以採用異步重試,就是把重試請求寫到消息隊列,由專門的消費者來重試,直到成功

或者更直接的做法,爲了避免第二步執行失敗,我們可以把操作緩存這一步,直接放到消息隊列中,由消費者來操作緩存,這樣做的好處是即使系統重啓了,消息也不會丟失

也可以通過訂閱數據庫變更日誌,再操作緩存的方式,以 MySQL 舉例,當一條數據發生修改時,MySQL 就會產生一條變更日誌(Binlog),我們可以訂閱這個日誌,拿到具體操作的數據,然後再根據這條數據,去刪除對應的緩存。訂閱變更日誌,目前也有了比較成熟的開源中間件,例如阿里的 canal

2. 延遲雙刪

一般數據庫會使用【主從複製 + 讀寫分離】提高性能,這種情況下也有可能出現數據不一致:

  • 線程 A 更新主庫 X = 2(原值 X = 1)
  • 線程 A 刪除緩存
  • 線程 B 查詢緩存,沒有命中,查詢「從庫」得到舊值(從庫 X = 1)
  • 從庫「同步」完成(主從庫 X = 2)
  • 線程 B 將「舊值」寫入緩存(X = 1)
  • 最終 X 的值在緩存中是 1(舊值),在主從庫中是 2(新值),也發生不一致

解決辦法就是延時雙刪,比如線程 A 在更新數據庫並刪除緩存後,延遲一段時間再刪除一次,延遲時間取決於主從複製的延遲時間,一般憑經驗估算 1s - 5s 左右

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