什麼是快取?
快取就是資料交換的緩衝區,針對服務物件的不同(本質就是不同的硬體)都可以構建快取。
目的是,把讀寫速度慢的介質的資料儲存在讀寫速度快的介質中,從而提高讀寫速度,減少時間消耗。 例如:
- 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 日誌
- 應用直接寫資料到資料庫中。
- 資料庫更新 binlog 日誌。
- 利用 Canal 中介軟體讀取 binlog 日誌。
- Canal 藉助於限流元件按頻率將資料發到 MQ 中。
- 應用監控 MQ 通道,將 MQ 的資料更新到 Redis 快取中。
可以看到這種方案對研發人員來說比較輕量,不用關心快取層面,而且這個方案雖然比較重,但是卻容易形成統一的解決方案。
備註說明: 上述的訂閱 binlog 程式在 mysql 中有現成的中介軟體叫 canal,可以完成訂閱 binlog 日誌的功能。
參考文章: