當執行寫操作后,需要保證從緩存讀取到的數據與數據庫中持久化的數據是一致的,因此需要對緩存進行更新。
因為涉及到數據庫和緩存兩步操作,難以保證更新的原子性。
在設計更新策略時,我們需要考慮多個方面的問題:
對系統吞吐量的影響:比如更新緩存策略產生的數據庫負載小于刪除緩存策略的負載
并發安全性:并發讀寫時某些異常操作順序可能造成數據不一致,如緩存中長期保存過時數據
更新失敗的影響:若某個操作失敗,如何對業務影響降到最小
檢測和修復故障的難度: 操作失敗導致的錯誤會在日志留下詳細的記錄容易檢測和修復。并發問題導致的數據錯誤沒有明顯的痕跡難以發現,且在流量高峰期更容易產生并發錯誤產生的業務風險較大。
更新緩存有兩種方式:
刪除失效緩存: 讀取時會因為未命中緩存而從數據庫中讀取新的數據并更新到緩存中
更新緩存: 直接將新的數據寫入緩存覆蓋過期數據
更新緩存和更新數據庫有兩種順序:
先數據庫后緩存
先緩存后數據庫
兩兩組合共有四種更新策略,現在我們逐一進行分析。
并發問題通常由于后開始的線程卻先完成操作導致,我們把這種現象稱為“搶跑”。下面我們逐一分析四種策略中“搶跑”帶來的錯誤。
先更新數據庫,再刪除緩存
若數據庫更新成功,刪除緩存操作失敗,則此后讀到的都是緩存中過期的數據,造成不一致問題。
可能存在讀寫線程競爭導致的并發錯誤:
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
先更新數據庫,再更新緩存
同刪除緩存策略一樣,若數據庫更新成功緩存更新失敗則會造成數據不一致問題。
該策略同樣存在讀寫線程競爭導致數據不一致的問題:
也可能因為兩個寫線程競爭導致并發錯誤:
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
先刪除緩存,再更新數據庫
可能發生的并發錯誤:
先更新緩存,再更新數據庫
若緩存更新成功數據庫更新失敗, 則此后讀到的都是未持久化的數據。因為緩存中的數據是易失的,這種狀態非常危險。
因為數據庫因為鍵約束導致寫入失敗的可能性較高,所以這種策略風險較大。
可能發生的并發錯誤:
兩個寫線程競爭也會導致數據不一致:
解決方案
使用 CAS
CAS (Check-And-Set 或 Compare-And-Swap)是一種常見的保證并發安全的手段。CAS 當且僅當客戶端最后一次取值后該 key 沒有被其他客戶端修改的情況下,才允許當前客戶端將新值寫入。
funcCAS(oldVal,newVal){ ifcache.get()==oldVal{ cache.set(newVal) } }
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
0 | v0 | v0 | ||
1 | 更新數據庫為 v1 | v1 | v0 | |
2 | 更新數據庫為 v2 | v2 | v0 | |
3 | 執行 CAS 操作:當且僅當緩存中為 v0 時將 v2 寫入緩存 | v2 | v2 | |
4 | 執行 CAS 操作:當且僅當緩存中為 v0 時將v1寫入緩存。當前緩存為 v2 故放棄寫緩存 | v2 | v2 |
由上圖可見,CAS 可以有效的避免并發錯誤的發生。
目前一些兼容 Redis 協議的中間件已經提供了 CAS 命令的支持,比如阿里的 Tair 以及騰訊的 Tendis。
Redis 官方提供了 Watch + 事務的方法來支持 CAS, 或者使用 redis 中 lua 腳本原子性執行的特點來實現 CAS。不過由于代碼較為復雜,這兩種方案都不常見。
使用分布式鎖
CAS 假設發生并發問題的概率不大, 所以 CAS 也被稱為樂觀鎖。那么悲觀鎖能否解決我們的問題呢?
還是以「先更新數據庫,再更新緩存」方案中兩個寫線程競爭為例, 我們要求任何線程在寫入或讀取數據庫前都需要獲取排它鎖。
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
0 | v0 | v0 | ||
1 | 獲取排它鎖 | v0 | v0 | |
2 | 更新數據庫為 v1 | v1 | v0 | |
3 | 更新緩存為 v1 | v1 | v1 | |
4 | 等待排它鎖 | v1 | v1 | |
5 | 釋放排它鎖 | v1 | v1 | |
6 | 獲得排它鎖 | v1 | v1 | |
7 | 更新數據庫為 v2 | v2 | v1 | |
8 | 更新緩存為 v2 | v2 | v2 | |
9 | 釋放排它鎖 | v2 | v2 |
分布式鎖同樣可以解決并發問題,只是成本可能略高。
異步更新
阿里開源了 MySQL 數據庫binlog的增量訂閱和消費組件 - canal。canal 模擬從庫獲得主庫的 binlog 更新,然后將更新數據寫入 MQ 或直接進行消費。
我們可以讓API服務器只負責寫入數據庫,另一個線程訂閱數據庫 binlog 增量進行緩存更新。
因為 binlog 是有序的,因此可以避免兩個寫線程競爭。但我們仍然需要解決讀寫線程競爭的問題:
這里同樣可以 CAS 解千愁:
延時雙刪
使用刪除緩存策略時讀線程先開始卻后寫緩存會導致不一致,那么我們在讀線程結束后再次清除緩存是不是就可以解除錯誤狀態了?延時雙刪就是寫線程等待一段時間“確保”讀線程都結束后再次刪除緩存,以此清除可能的錯誤緩存數據。
理論上我們無法給出一個時間來“確?!弊x線程都結束,所以仍有存在并發問題的可能。但是延時雙刪實現成本很低而且極大的減少了并發問題出現的概率,不失為一種簡單實用的手段。
審核編輯:劉清
-
CAS
+關注
關注
0文章
34瀏覽量
15201 -
MYSQL數據庫
+關注
關注
0文章
96瀏覽量
9389 -
Redis
+關注
關注
0文章
374瀏覽量
10871
原文標題:講講 Redis 緩存更新一致性
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論