Redis與MySQL數(shù)據(jù)一致性問題的策略模式及解決方案
在開發(fā)中,一般會使用Redis緩存一些常用的熱點數(shù)據(jù)用來減少數(shù)據(jù)庫IO,提高系統(tǒng)的吞吐量
先了解一下分布式系統(tǒng)中的一致性概念。
強一致性:所有節(jié)點的數(shù)據(jù)必須實時同步,保證任何時候讀取到的數(shù)據(jù)都是最新的。
弱一致性:系統(tǒng)允許數(shù)據(jù)暫時不一致,但最終會達到一致狀態(tài)。
最終一致性:數(shù)據(jù)更新后,經(jīng)過一段時間,系統(tǒng)會逐步達到一致狀態(tài)。這個時間不固定,但在業(yè)務允許的范圍內(nèi)。
雙寫一致性:當數(shù)據(jù)同時存在于緩存(Redis)和數(shù)據(jù)庫(MySQL)時,兩者之間數(shù)據(jù)一致
那么容易出現(xiàn)數(shù)據(jù)一致性問題的場景是:
- 數(shù)據(jù)寫入數(shù)據(jù)庫,未更新緩存
- 刪除緩存后,數(shù)據(jù)庫更新失敗
一、策略模式
緩存可以提升性能、緩解數(shù)據(jù)庫壓力,但是使用緩存也會導致數(shù)據(jù)不一致性的問題。有三種經(jīng)典的緩存使用模式:
- Cache-Aside Pattern
- Read-Through/Write-through
- Write-behind
1、旁路緩存模式(Cache Aside Pattern)
Cache Aside Pattern的提出是為了盡可能地解決緩存與數(shù)據(jù)庫的數(shù)據(jù)不一致問題
流程:
- 讀取操作:先從緩存中讀取數(shù)據(jù),緩存命中返回結(jié)果;緩存未命中,從DB中讀取數(shù)據(jù),并將數(shù)據(jù)寫入緩存。
- 更新操作:先更DB,再刪除緩存中的舊數(shù)據(jù)。
在日常開發(fā)中,一般使用了Cache Aside Pattern緩存更新策略模式,以數(shù)據(jù)庫為主,緩存為輔
public class CacheAsidePattern {
private RedisService redis;
private DatabaseService database;
// 讀取操作
public String getData(String key) {
// 從緩存中獲取數(shù)據(jù)
String value = redis.get(key);
if (value == null) {
// 緩存未命中,從數(shù)據(jù)庫獲取數(shù)據(jù)
value = database.get(key);
if (value != null) {
// 將數(shù)據(jù)寫入緩存
redis.set(key, value);
}
}
return value;
}
// 更新操作
public void updateData(String key, String value) {
// 更新數(shù)據(jù)庫
database.update(key, value);
// 刪除緩存中的舊數(shù)據(jù)
redis.delete(key);
}
}?:Cache-Aside在操作數(shù)據(jù)庫時,為什么是先操作數(shù)據(jù)庫呢?為什么不先操作緩存呢?
1、先刪除緩存后,數(shù)據(jù)庫更新失敗
線程1:刪除緩存A,由于網(wǎng)絡問題沒有操作數(shù)據(jù)庫失敗
線程2:查詢A,緩存無數(shù)據(jù),并把A寫入緩存
線程1:網(wǎng)絡堵塞結(jié)束,修改數(shù)據(jù)庫A為B
那么此時緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦!??!

因此,Cache-Aside緩存模式,選擇了先操作數(shù)據(jù)庫而不是先操作緩存
2、先操作數(shù)據(jù)庫再刪除緩存方案
線程1:操作數(shù)據(jù)庫,A更新數(shù)據(jù)為B,刪除緩存A
線程2:查詢A,緩存無數(shù)據(jù),并把B寫入緩存

這種方案下,在數(shù)據(jù)庫更新成功后到刪除Redis緩存數(shù)據(jù)之前的這段時間中,其他線程讀取的數(shù)據(jù)都是舊數(shù)據(jù),等Redis刪除緩存后會重新從數(shù)據(jù)庫中讀取最新數(shù)據(jù)同步到Redis,這樣可以在一定程度上保證數(shù)據(jù)的最終一致性
但是在極端情況下,線程1的緩存刪除失敗,線程2讀取的也就是舊數(shù)據(jù)A,而不是新數(shù)據(jù)B了
這種方案也就是旁路緩存模式,那么Cache-Aside的優(yōu)缺點就是:
優(yōu)點:
簡單易懂,易于實現(xiàn)
讀性能高,因為大部分讀操作都會命中緩存
缺點:
更新數(shù)據(jù)庫后緩存可能還沒刪除,存在短暫的不一致
刪除緩存后,如果數(shù)據(jù)庫更新失敗,會導致數(shù)據(jù)不一致
?:Cache-Aside在寫入請求的時候,為什么是刪除緩存而不是更新緩存呢?
線程1:操作數(shù)據(jù)庫,更新數(shù)據(jù)為A,由于網(wǎng)絡問題未更新緩存
線程2:操作數(shù)據(jù)庫,更新數(shù)據(jù)為B,更新緩存為B
線程1:網(wǎng)絡堵塞結(jié)束,更新緩存為A
那么此時緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦?。。?/p>
如果是刪除緩存取代更新緩存則不會出現(xiàn)這個臟數(shù)據(jù)問題?。?!
因此,Cache-Aside緩存模式,選擇了刪除緩存而不是更新緩存
適應場景:適用于讀多寫少的場景,特別是對數(shù)據(jù)一致性要求不是特別高的應用
2、讀寫穿透(Read-Through/Write-Through)
Read-Through:當緩存未命中時,自動從數(shù)據(jù)庫加載數(shù)據(jù),并寫入緩存
Write-Through:當緩存更新時,同步將數(shù)據(jù)寫入數(shù)據(jù)庫
和旁路緩存模式很像,只有寫操作不太一樣
public class ReadWriteThroughPattern {
private RedisService redis;
private DatabaseService database;
// Read-Through
public String readThrough(String key) {
// 從緩存中獲取數(shù)據(jù)
String value = redis.get(key);
if (value == null) {
// 緩存未命中,從數(shù)據(jù)庫獲取數(shù)據(jù)
value = database.get(key);
if (value != null) {
// 將數(shù)據(jù)寫入緩存
redis.set(key, value);
}
}
return value;
}
// Write-Through
public void writeThrough(String key, String value) {
// 將數(shù)據(jù)寫入緩存
redis.set(key, value);
// 同步將數(shù)據(jù)寫入數(shù)據(jù)庫
database.update(key, value);
}
}優(yōu)點:
- 保證了數(shù)據(jù)的強一致性,緩存和數(shù)據(jù)庫的數(shù)據(jù)始終同步。
- 讀寫操作都由緩存處理,數(shù)據(jù)庫壓力較小。
缺點:
- 寫操作的延遲較高,因為每次寫入緩存時都需要同步寫入數(shù)據(jù)庫,增加了系統(tǒng)的響應時間
- 實現(xiàn)復雜度較高,需要額外的緩存同步機制
適應場景:適合讀多寫多、且對數(shù)據(jù)一致性要求較高的場景
3、異步緩存寫入(Write Behind)
異步緩存就是緩存更新后,異步批量寫入數(shù)據(jù)庫。這種策略適用于可以容忍一定數(shù)據(jù)不一致的高性能場景
示例代碼:
public class WriteBehindPattern {
private RedisService redis;
private DatabaseService database;
private UpdateQueue updateQueue;
// 異步緩存寫入
public void writeBehind(String key, String value) {
// 將數(shù)據(jù)寫入緩存
redis.set(key, value);
// 異步將數(shù)據(jù)寫入數(shù)據(jù)庫
asyncDatabaseUpdate(key, value);
}
private void asyncDatabaseUpdate(String key, String value) {
// 異步操作,將更新請求放入隊列
updateQueue.add(new UpdateTask(key, value));
}
}優(yōu)點:
寫操作的性能非常高,因為只需更新緩存,數(shù)據(jù)庫更新是異步進行的
適用于對寫操作性能要求較高的場景
缺點:
存在數(shù)據(jù)不一致的風險,緩存更新后數(shù)據(jù)庫可能還未更新。
實現(xiàn)復雜度較高,需要處理異步操作中的異常和重試
適應場景:大批量數(shù)據(jù)讀取,允許短期數(shù)據(jù)不一致,寫密集型場景
二、一致性解決方案
緩存系統(tǒng)適用的場景就是非強一致性的場景,它屬于CAP中的AP
CAP理論,指的是在一個分布式系統(tǒng)中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區(qū)容錯性),三者不可得兼。
沒辦法做到數(shù)據(jù)庫與緩存絕對的一致性,但通過一些方案優(yōu)化處理,是可以保證弱一致性,最終一致性的
1、緩存延遲雙刪
流程:
- 先刪除緩存
- 再更新數(shù)據(jù)庫
- 休眠一會(比如1秒),再次刪除緩存
但休眠的時間內(nèi),可能有臟數(shù)據(jù),且第二次刪除也可能失敗,導致的數(shù)據(jù)不一致問題
延遲雙刪策略只能保證最終的一致性,不能保證強一致性。由于對Redis的操作和Mysql的操作不是原子性操作,所以如果想保證數(shù)據(jù)的強一致性就需要加鎖控制,如下圖所示

加鎖之后勢必會帶來系統(tǒng)的吞吐量的下降,所以需要衡量利弊來確定是否使用加鎖
方案優(yōu)化:刪除失敗就多刪除幾次呀,保證刪除緩存成功就可以了!
所以可以引入刪除緩存重試機制
2、刪除重試機制
刪除緩存失敗,則將這些key放入到消息隊列中,消費消息隊列的消息,獲取要刪除的key,重試刪除緩存操作
3、讀取biglog異步刪除緩存
重試刪除緩存機制還可以吧,就是會造成好多業(yè)務代碼入侵。
方案優(yōu)化:通過數(shù)據(jù)庫的binlog來異步淘汰key

以MySQL為例,通過canal監(jiān)聽binlog日志感知數(shù)據(jù)的變動后,canal客戶端執(zhí)行刪除Redis緩存數(shù)據(jù),如果緩存數(shù)據(jù)刪除失敗那么發(fā)送一條MQ消息讓canal客戶端繼續(xù)執(zhí)行刪除操作,這樣可以保證數(shù)據(jù)的最終一致性,但是這樣也增加了系統(tǒng)的復雜性
三、總結(jié)
(1)實際開發(fā)中一般使用使用了Cache Aside Pattern緩存更新策略模式,此方案最大程度上保證了數(shù)據(jù)的一致性并且實現(xiàn)也最簡單
(2)無論是先操作數(shù)據(jù)庫再刪除緩存還是先刪除緩存再操作數(shù)據(jù)庫都有可能會出現(xiàn)刪除緩存失敗的情況,所以需要加入刪除重試機制
(3)如果想要Redis和Mysql的數(shù)據(jù)強一致性,可以考慮使用加鎖的方式實現(xiàn)
以上就是Redis與MySQL數(shù)據(jù)一致性問題的策略模式及解決方案的詳細內(nèi)容,更多關(guān)于Redis與MySQL數(shù)據(jù)一致性的資料請關(guān)注腳本之家其它相關(guān)文章!
- Redis和數(shù)據(jù)庫的一致性(Canal+MQ) 的實現(xiàn)
- 使用Canal實現(xiàn)MySQL數(shù)據(jù)同步的完整指南
- canal實現(xiàn)mysql數(shù)據(jù)同步的詳細過程
- 兩個windows服務器使用canal實現(xiàn)mysql實時同步
- Canal實現(xiàn)MYSQL實時數(shù)據(jù)同步的示例代碼
- Canal進行MySQL到MySQL數(shù)據(jù)庫全量+增量同步踩坑指南
- 基于Docker結(jié)合Canal實現(xiàn)MySQL實時增量數(shù)據(jù)傳輸功能
- MySQL數(shù)據(jù)實時同步Redis的方案全解析
- 保證MySQL與Redis數(shù)據(jù)一致性的6種實現(xiàn)方案
- 詳解讓MySQL和Redis數(shù)據(jù)保持一致的四種策略
- Java使用Canal同步MySQL數(shù)據(jù)到Redis
- Linux寶塔面板使用Canal實現(xiàn)Mysql和Redis數(shù)據(jù)同步(圖文教程)
相關(guān)文章
Redis實戰(zhàn)之百度首頁新聞熱榜的實現(xiàn)代碼
這篇文章主要介紹了Redis實戰(zhàn)之百度首頁新聞熱榜的實現(xiàn)代碼,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
使用RediSearch實現(xiàn)在Redis中全文檢索
RediSearch?是?Redis?的一個插件,它為?Redis?數(shù)據(jù)庫添加了全文搜索和查詢功能,使開發(fā)人員能夠在?Redis?中高效地執(zhí)行全文檢索操作,下面我們就來看看是具體如何使用的吧2023-08-08

