快取與資料庫的一致性

Rabbit_Judy發表於2019-04-01

什麼是快取?

快取就是資料交換的緩衝區,針對服務物件的不同(本質就是不同的硬體)都可以構建快取。

目的是,把讀寫速度慢的介質的資料儲存在讀寫速度快的介質中,從而提高讀寫速度,減少時間消耗。 例如:

  • CPU 快取記憶體 :快取記憶體的讀寫速度遠高於記憶體。
    • CPU 讀資料時,如果在快取記憶體中找到所需資料,就不需要讀記憶體
    • CPU 寫資料時,先寫到快取記憶體,再回寫到記憶體。
  • 磁碟快取:磁碟快取其實就把常用的磁碟資料儲存在記憶體中,記憶體讀寫速度也是遠高於磁碟的。
    • 讀資料時,從記憶體讀取。
    • 寫資料時,可先寫到記憶體,定時或定量回寫到磁碟,或者是同步回寫。

為什麼要用快取?

使用快取的目的,就是提升讀寫效能。而實際業務場景下,更多的是為了提升讀效能,帶來更好的效能,更高的併發量。

日常業務中,我們使用比較多的資料庫是 MySQL,快取是 Redis 。Redis 比 MySQL 的讀寫效能好很多。那麼,我們將 MySQL 的熱點資料,快取到 Redis 中,提升讀取效能,也減小 MySQL 的讀取壓力。例如說:

  • 論壇帖子的訪問頻率比較高,且要實時更新閱讀量,使用 Redis 記錄帖子的閱讀量,可以提升效能和併發。
  • 商品資訊,資料更新的頻率不高,但是讀取的頻率很高,特別是熱門商品。

一致性問題是分散式常見的問題,分為最終一致性和強一致性。如果對資料有強一致性要求,不能放快取,我們只能保證最終一致性。

快取與資料庫的一致性問題?

問題的產生

  • 併發場景下,讀取舊的 DB 資料並更新到快取中。
  • 快取和 DB 的操作不在一個事務中,可能只有一個操作成功,另一個操作失敗,導致不一致。

解決方案

先淘汰快取,再更新資料庫

先淘汰快取,即使寫資料庫發生異常,也就是下次快取讀取時,多讀取一次資料庫,這樣理論上來說保證了資料的 一致性。但是實際在併發環境下仍然會出現資料不一致的情況。

首先來說寫流程:先淘汰快取,再寫 DB;

然後是讀流程:先讀快取,如果未命中再讀 DB,然後將 DB 中讀出來的資料更新到快取中。併發環境下,在資料庫層面併發的讀寫並不能保證完全順序,也就是說後發出的讀請求可能先完成。舉例來說

  • 執行緒 T1 發出了寫請求,淘汰了快取;然後寫資料庫,發出修改請求。
  • 執行緒 T2 發出了讀請求,先讀取快取,未命中,再去讀取資料庫,發出讀取請求,這時候,執行緒 T1 的寫資料還未完成,導致 T2 讀取了一個髒資料放入快取,這樣就導致了資料不一致。

這種情況下,可以引入分散式鎖實現“序列化”來解決。

  • 在寫請求時,先獲取分散式鎖,再淘汰快取,更新完資料庫後再釋放鎖。
  • 在讀請求時,如果快取未命中,則先獲取分散式鎖,加鎖失敗說明寫請求還未完成,繼續等待;加鎖成功則說明寫請求已完成,再去快取查詢一次,如果未命中,則再去 DB 查詢並即使更新到快取。

先寫資料庫,再更新快取

由於操作快取和運算元據庫不是原子的,則第一步寫資料庫操作成功,第二步淘汰快取失敗,就會出現 DB 中是新資料,快取中是舊資料,資料不一致的情況。在這種邏輯下,只有保證寫資料庫和更新快取在同一個事務中,才能保證最終一致性。

基於定時任務來實現

  • 先寫入資料庫,然後在寫入資料庫所在的事務中,插入一條記錄到任務表,該記錄會儲存需要更新的快取 key 和 value。
  • 定時任務每秒掃描任務表,更新到快取中,之後刪除該記錄。

基於訊息佇列實現

  • 首先寫入資料庫,然後傳送帶有快取 key 和 value 的事務訊息,此時需要有支援事務訊息特性的訊息佇列。
  • 消費者消費該訊息,更新到快取中。

基於資料庫的 binlog 日誌

image

  • 應用直接寫資料到資料庫中。
  • 資料庫更新 binlog 日誌。
  • 利用 Canal 中介軟體讀取 binlog 日誌。
  • Canal 藉助於限流元件按頻率將資料發到 MQ 中。
  • 應用監控 MQ 通道,將 MQ 的資料更新到 Redis 快取中。

可以看到這種方案對研發人員來說比較輕量,不用關心快取層面,而且這個方案雖然比較重,但是卻容易形成統一的解決方案。

備註說明: 上述的訂閱 binlog 程式在 mysql 中有現成的中介軟體叫 canal,可以完成訂閱 binlog 日誌的功能。

參考文章:

精盡【快取】面試題

相關文章