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