持久化數據&緩存數據雙寫一致性

背景

緩存中數據更新一般有兩個入口,

  • 數據緩存過期,數據在訪問時發現緩存中無數據時重新查庫然後更新至緩存;
    • 場景和問題等同於緩存查詢,相關solution參考“緩存數據查詢的注意事項”;
  • 緩存未過期,數據庫數據有變動主動更新至緩存;
    • 比較常見的場景;
    • 也即爲雙寫的概念:有新版本的數據需要同時寫入持久化層和緩存層中;

問題點

數據一致性

  • 是指數據庫持久化數據與高速緩存庫中緩存數據的不一致;
  • 一般傾向於數據庫爲準,也就是說不一致是指緩存數據的時效性保障性低於持久化數據;
  • 不一致也即爲緩存數據爲髒數據;

髒數據的概念

  • 緩存的更新過程可能會出現時間上或長或短的時效性過期情況(或者說無法避免);
  • 長期或者在緩存單位生命週期中是髒數據的情況是無法接受的;
  • 但是如果在實現級出現短暫的髒數據,通過兜底或者其他策略完整最終一致性是解決方向;

緩存的過期時間

  • 常見緩存過期時間的處理策略是
    • 設置指定的過期時間:經過過期時間之後在數據查詢過程中綁定緩存初始化的流程實現更新;
    • 不設置過期時間:長期有效,只有主動發起纔會執行緩存的數據更新;
  • 關於數據一致性
    • 上述的設置過期時間的策略也是對數據一致問題的一種強行兜底方案;
      • 只是兜底,並不能依賴,緩存單位生命週期長度的髒數據無法接受;
    • 需要將髒數據的存在時間範圍放在可控範圍內;
      • 可控範圍:業務可接受的範圍;

問題解析

排除因素

  • 在分析一致性問題時我們排除使用過期時間兜底實現一致性的策略;
    • 首先,只是最終兜底,並且業務不可接受;
    • 以不過期的使用背景來分析,問題更明顯;

問題來源

  • 無事務:緩存的操作不被包含在事務控制中;
    • 典型的情況:緩存更新失敗,但是不影響數據庫事務的成功提交,導致不一致;
  • 併發:因爲無事務,無法保證雙寫的一致性;
    • 併發導致的時序問題導致數據覆蓋,可能出現有效數據是舊數據的情況;
  • 多數據源:如果拿着一份數據分別向持久層和緩存執行執行更新,在時序無法保證時會出現交叉覆蓋的情況;
  • 互斥:緩存的初始化(查詢數據+set緩存)需要保證原子性,並且初始化和緩存的淘汰需要互斥,否則併發下交叉執行會有舊數據覆蓋情況;

多數據源的描述實際也是因爲無事務+併發造成的;

解決方向

  • 唯一數據源:
    • 以持久化數據爲準,由緩存依賴獲取持久化數據來實現更新;
  • 事務、併發:
    • 雖無法將緩存操作加入事務,但是需要保證緩存的操作在數據庫事務提交之後,保證緩存更新依賴的數據有效性;
    • 快速兜底:在出現異常情況時通過重試機制快速兜底或預警通知;
  • 保證互斥:
    • 緩存的清楚和初始化使用鎖保證互斥;

方案

更新緩存

更新緩存or淘汰緩存(刪除緩存)
根據上述的通過保證唯一數據源來避免問題,應該採用淘汰緩存,然後緩存通過從持久化層獲取數據完成初始化;

代表方案

如果採用更新緩存,無論更新緩存和更新數據庫前後順序怎麼排列;
即無論是“更新緩存+更新數據庫”還是“更新數據庫+更新緩存”都會有以下問題;

問題:

  • 併發:
    • 如果多線程執行時序發生交叉,即ABBA或者BAAB時,持久層由於有數據庫事務的保證,數據最終是有效的,但是緩存可能會發生舊數據覆蓋新數據的情況;
  • 事務:
    • “更新數據庫+更新緩存”,假如“更新緩存“的環節失敗,但是“更新數據庫”事務已經提交,導致不一致,這個問題需要我們關注,包括後續方案也需要關注這個問題;
    • “更新緩存+更新數據庫”,這個就會有致命問題,即新數據已經更新至緩存,但是“更新數據庫”時失敗了,反而導致緩存數據是未持久化的數據;

結論

使用淘汰緩存,而不是更新緩存;

“淘汰緩存+更新數據庫”

問題:

  • 事務+併發:
    • 因爲兩個環節的操作無事務統一管理,沒有原子性;
      • 則可能在淘汰緩存之後、更新數據庫事務提交之前;由其他線程訪問緩存數據,觸發緩存刷新流程,此時持久層數據仍是舊數據,而且被刷新至緩存,導致不一致;

結論:

  • 無法保證一致性,隱患很大;

“更新數據庫+淘汰緩存”

細節

  • 保證“淘汰緩存”在“更新數據”事務提交之後,即:保證持久層有新數據之後再執行緩存淘汰;

問題:

  • 事務+兜底:
    • 此時,如果持久層事務提交之後,淘汰緩存因爲各種原因失敗,導致不一致;

一個細節問題(關於緩存操作的互斥):

- 如果不考慮上述緩存操作異常情況;
- 更新緩存時如果緩存未超時,此場景是無問題的;
-  場景:如果緩存數據是有效期的或者其他操作,導致在執行“更新數據庫”之前 原緩存數據過期,此時有其它線程訪問緩存數據並觸發了緩存初始化流程:
    A_查詢持久化數據+B_將查詢結果set至緩存;那麼就會有一個細節上的問題了:
    - 如果上述兩個環節A&B都在當前線程的C_淘汰緩存這個環節之前完成,那就不會有問題,最後結果是再次淘汰緩存,保證了一致性;
    - 如果上述兩個環節A&B交叉在C之間,即執行時序是A-C-B,那麼上述的C_淘汰緩存操作就無意義了,還是會導致不一致;
- 解決方案:
    - 因爲緩存的初始化是有同步鎖控制的,如果將緩存淘汰操作也加入同一個同步鎖控制內,就可以避免上述A-C-B的問題;並且不會造成額外影響;

結論:

  • 邏輯上和可行性個人認爲是最靠譜的一個;
    • 通過消息支持重試機制進行兜底;
    • 通過同步鎖與緩存初始化互斥保證邏輯完整;
  • 邏輯上是符合常理的;並且數據持久化完成之後“更新緩存”操作失敗的兜底機制和保證緩存的互斥性是需要關注的核心問題;

數據庫主從同步延遲的問題

問題描述:

上述的數據持久化都是在講的概念,實際上如果存在讀寫分離,從庫從主庫同步數據需要一定的時間,即使在保證上述邏輯正常執行的情況下,
也需要考慮,當新數據成功持久化至主庫之後、從庫同步數據之前,有訪問數據的請求落在從庫上,讀到的還是舊數據,仍然會造成髒數據;

解決思路:

訂閱數據庫binlog日誌,在數據完成同步的時候重新發起“淘汰緩存”操作,並輔以消息重試機制保證執行;

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