當(dāng)執(zhí)行寫操作后,需要保證從緩存讀取到的數(shù)據(jù)與數(shù)據(jù)庫中持久化的數(shù)據(jù)是一致的,因此需要對緩存進行更新。
因為涉及到數(shù)據(jù)庫和緩存兩步操作,難以保證更新的原子性。
在設(shè)計更新策略時,我們需要考慮多個方面的問題:
對系統(tǒng)吞吐量的影響:比如更新緩存策略產(chǎn)生的數(shù)據(jù)庫負(fù)載小于刪除緩存策略的負(fù)載
并發(fā)安全性:并發(fā)讀寫時某些異常操作順序可能造成數(shù)據(jù)不一致,如緩存中長期保存過時數(shù)據(jù)
更新失敗的影響:若某個操作失敗,如何對業(yè)務(wù)影響降到最小
檢測和修復(fù)故障的難度: 操作失敗導(dǎo)致的錯誤會在日志留下詳細(xì)的記錄容易檢測和修復(fù)。并發(fā)問題導(dǎo)致的數(shù)據(jù)錯誤沒有明顯的痕跡難以發(fā)現(xiàn),且在流量高峰期更容易產(chǎn)生并發(fā)錯誤產(chǎn)生的業(yè)務(wù)風(fēng)險較大。
更新緩存有兩種方式:
刪除失效緩存: 讀取時會因為未命中緩存而從數(shù)據(jù)庫中讀取新的數(shù)據(jù)并更新到緩存中
更新緩存: 直接將新的數(shù)據(jù)寫入緩存覆蓋過期數(shù)據(jù)
更新緩存和更新數(shù)據(jù)庫有兩種順序:
先數(shù)據(jù)庫后緩存
先緩存后數(shù)據(jù)庫
兩兩組合共有四種更新策略,現(xiàn)在我們逐一進行分析。
并發(fā)問題通常由于后開始的線程卻先完成操作導(dǎo)致,我們把這種現(xiàn)象稱為“搶跑”。下面我們逐一分析四種策略中“搶跑”帶來的錯誤。
先更新數(shù)據(jù)庫,再刪除緩存
若數(shù)據(jù)庫更新成功,刪除緩存操作失敗,則此后讀到的都是緩存中過期的數(shù)據(jù),造成不一致問題。
可能存在讀寫線程競爭導(dǎo)致的并發(fā)錯誤:
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
先更新數(shù)據(jù)庫,再更新緩存
同刪除緩存策略一樣,若數(shù)據(jù)庫更新成功緩存更新失敗則會造成數(shù)據(jù)不一致問題。
該策略同樣存在讀寫線程競爭導(dǎo)致數(shù)據(jù)不一致的問題:

也可能因為兩個寫線程競爭導(dǎo)致并發(fā)錯誤:

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項目地址: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ù)庫因為鍵約束導(dǎo)致寫入失敗的可能性較高,所以這種策略風(fēng)險較大。
可能發(fā)生的并發(fā)錯誤:

兩個寫線程競爭也會導(dǎo)致數(shù)據(jù)不一致:

解決方案
使用 CAS
CAS (Check-And-Set 或 Compare-And-Swap)是一種常見的保證并發(fā)安全的手段。CAS 當(dāng)且僅當(dāng)客戶端最后一次取值后該 key 沒有被其他客戶端修改的情況下,才允許當(dāng)前客戶端將新值寫入。
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 操作:當(dāng)且僅當(dāng)緩存中為 v0 時將 v2 寫入緩存 | v2 | v2 | |
| 4 | 執(zhí)行 CAS 操作:當(dāng)且僅當(dāng)緩存中為 v0 時將v1寫入緩存。當(dāng)前緩存為 v2 故放棄寫緩存 | v2 | v2 |
由上圖可見,CAS 可以有效的避免并發(fā)錯誤的發(fā)生。
目前一些兼容 Redis 協(xié)議的中間件已經(jīng)提供了 CAS 命令的支持,比如阿里的 Tair 以及騰訊的 Tendis。
Redis 官方提供了 Watch + 事務(wù)的方法來支持 CAS, 或者使用 redis 中 lua 腳本原子性執(zhí)行的特點來實現(xiàn) CAS。不過由于代碼較為復(fù)雜,這兩種方案都不常見。
使用分布式鎖
CAS 假設(shè)發(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服務(wù)器只負(fù)責(zé)寫入數(shù)據(jù)庫,另一個線程訂閱數(shù)據(jù)庫 binlog 增量進行緩存更新。
因為 binlog 是有序的,因此可以避免兩個寫線程競爭。但我們?nèi)匀恍枰鉀Q讀寫線程競爭的問題:

這里同樣可以 CAS 解千愁:

延時雙刪
使用刪除緩存策略時讀線程先開始卻后寫緩存會導(dǎo)致不一致,那么我們在讀線程結(jié)束后再次清除緩存是不是就可以解除錯誤狀態(tài)了?延時雙刪就是寫線程等待一段時間“確?!弊x線程都結(jié)束后再次刪除緩存,以此清除可能的錯誤緩存數(shù)據(jù)。

理論上我們無法給出一個時間來“確?!弊x線程都結(jié)束,所以仍有存在并發(fā)問題的可能。但是延時雙刪實現(xiàn)成本很低而且極大的減少了并發(fā)問題出現(xiàn)的概率,不失為一種簡單實用的手段。
審核編輯:劉清
-
CAS
+關(guān)注
關(guān)注
0文章
35瀏覽量
15617 -
MYSQL數(shù)據(jù)庫
+關(guān)注
關(guān)注
0文章
99瀏覽量
10302 -
Redis
+關(guān)注
關(guān)注
0文章
394瀏覽量
12258
原文標(biāo)題:講講 Redis 緩存更新一致性
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
介紹ARM存儲一致性模型的相關(guān)知識
如何解決數(shù)據(jù)庫與緩存一致性
Redis緩存和MySQL數(shù)據(jù)不一致原因和解決方案
一致性規(guī)劃研究
加速器一致性接口
基于軌跡標(biāo)簽的謠言一致性維護算法
Cache一致性協(xié)議優(yōu)化研究
自主駕駛系統(tǒng)將使用緩存一致性互連IP和非一致性互連IP
管理基于Cortex?-M7的MCU的高速緩存一致性
介紹下cpu緩存一致性(MESI協(xié)議)
管理基于Cortex-M7的MCU的高速緩存一致性
如何保證緩存一致性
redis與mysql如何保持?jǐn)?shù)據(jù)一致性
Redis緩存與Mysql如何保證一致性?
異構(gòu)計算下緩存一致性的重要性
Redis緩存更新一致性的方式
評論