Redis與MySQL數(shù)據(jù)一致性問題的策略模式及解決方案
在開發(fā)中,一般會使用Redis緩存一些常用的熱點(diǎn)數(shù)據(jù)用來減少數(shù)據(jù)庫IO,提高系統(tǒng)的吞吐量
先了解一下分布式系統(tǒng)中的一致性概念。
強(qiáng)一致性:所有節(jié)點(diǎn)的數(shù)據(jù)必須實(shí)時同步,保證任何時候讀取到的數(shù)據(jù)都是最新的。
弱一致性:系統(tǒng)允許數(shù)據(jù)暫時不一致,但最終會達(dá)到一致狀態(tài)。
最終一致性:數(shù)據(jù)更新后,經(jīng)過一段時間,系統(tǒng)會逐步達(dá)到一致狀態(tài)。這個時間不固定,但在業(yè)務(wù)允許的范圍內(nèi)。
雙寫一致性:當(dāng)數(shù)據(jù)同時存在于緩存(Redis)和數(shù)據(jù)庫(MySQL)時,兩者之間數(shù)據(jù)一致
那么容易出現(xiàn)數(shù)據(jù)一致性問題的場景是:
- 數(shù)據(jù)寫入數(shù)據(jù)庫,未更新緩存
- 刪除緩存后,數(shù)據(jù)庫更新失敗
一、策略模式
緩存可以提升性能、緩解數(shù)據(jù)庫壓力,但是使用緩存也會導(dǎo)致數(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)絡(luò)問題沒有操作數(shù)據(jù)庫失敗
線程2:查詢A,緩存無數(shù)據(jù),并把A寫入緩存
線程1:網(wǎng)絡(luò)堵塞結(jié)束,修改數(shù)據(jù)庫A為B
那么此時緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦?。。?/p>

因此,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)缺點(diǎn)就是:
優(yōu)點(diǎn):
簡單易懂,易于實(shí)現(xiàn)
讀性能高,因?yàn)榇蟛糠肿x操作都會命中緩存
缺點(diǎn):
更新數(shù)據(jù)庫后緩存可能還沒刪除,存在短暫的不一致
刪除緩存后,如果數(shù)據(jù)庫更新失敗,會導(dǎo)致數(shù)據(jù)不一致
?:Cache-Aside在寫入請求的時候,為什么是刪除緩存而不是更新緩存呢?
線程1:操作數(shù)據(jù)庫,更新數(shù)據(jù)為A,由于網(wǎng)絡(luò)問題未更新緩存
線程2:操作數(shù)據(jù)庫,更新數(shù)據(jù)為B,更新緩存為B
線程1:網(wǎng)絡(luò)堵塞結(jié)束,更新緩存為A
那么此時緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦?。。?/p>
如果是刪除緩存取代更新緩存則不會出現(xiàn)這個臟數(shù)據(jù)問題?。?!
因此,Cache-Aside緩存模式,選擇了刪除緩存而不是更新緩存
適應(yīng)場景:適用于讀多寫少的場景,特別是對數(shù)據(jù)一致性要求不是特別高的應(yīng)用
2、讀寫穿透(Read-Through/Write-Through)
Read-Through:當(dāng)緩存未命中時,自動從數(shù)據(jù)庫加載數(shù)據(jù),并寫入緩存
Write-Through:當(dāng)緩存更新時,同步將數(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)點(diǎn):
- 保證了數(shù)據(jù)的強(qiáng)一致性,緩存和數(shù)據(jù)庫的數(shù)據(jù)始終同步。
- 讀寫操作都由緩存處理,數(shù)據(jù)庫壓力較小。
缺點(diǎn):
- 寫操作的延遲較高,因?yàn)?strong>每次寫入緩存時都需要同步寫入數(shù)據(jù)庫,增加了系統(tǒng)的響應(yīng)時間
- 實(shí)現(xiàn)復(fù)雜度較高,需要額外的緩存同步機(jī)制
適應(yīng)場景:適合讀多寫多、且對數(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) {
// 異步操作,將更新請求放入隊(duì)列
updateQueue.add(new UpdateTask(key, value));
}
}優(yōu)點(diǎn):
寫操作的性能非常高,因?yàn)橹恍韪戮彺妫瑪?shù)據(jù)庫更新是異步進(jìn)行的
適用于對寫操作性能要求較高的場景
缺點(diǎn):
存在數(shù)據(jù)不一致的風(fēng)險(xiǎn),緩存更新后數(shù)據(jù)庫可能還未更新。
實(shí)現(xiàn)復(fù)雜度較高,需要處理異步操作中的異常和重試
適應(yīng)場景:大批量數(shù)據(jù)讀取,允許短期數(shù)據(jù)不一致,寫密集型場景
二、一致性解決方案
緩存系統(tǒng)適用的場景就是非強(qiáng)一致性的場景,它屬于CAP中的AP
CAP理論,指的是在一個分布式系統(tǒng)中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區(qū)容錯性),三者不可得兼。
沒辦法做到數(shù)據(jù)庫與緩存絕對的一致性,但通過一些方案優(yōu)化處理,是可以保證弱一致性,最終一致性的
1、緩存延遲雙刪
流程:
- 先刪除緩存
- 再更新數(shù)據(jù)庫
- 休眠一會(比如1秒),再次刪除緩存
但休眠的時間內(nèi),可能有臟數(shù)據(jù),且第二次刪除也可能失敗,導(dǎo)致的數(shù)據(jù)不一致問題
延遲雙刪策略只能保證最終的一致性,不能保證強(qiáng)一致性。由于對Redis的操作和Mysql的操作不是原子性操作,所以如果想保證數(shù)據(jù)的強(qiáng)一致性就需要加鎖控制,如下圖所示

加鎖之后勢必會帶來系統(tǒng)的吞吐量的下降,所以需要衡量利弊來確定是否使用加鎖
方案優(yōu)化:刪除失敗就多刪除幾次呀,保證刪除緩存成功就可以了!
所以可以引入刪除緩存重試機(jī)制
2、刪除重試機(jī)制
刪除緩存失敗,則將這些key放入到消息隊(duì)列中,消費(fèi)消息隊(duì)列的消息,獲取要刪除的key,重試刪除緩存操作
3、讀取biglog異步刪除緩存
重試刪除緩存機(jī)制還可以吧,就是會造成好多業(yè)務(wù)代碼入侵。
方案優(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)的復(fù)雜性
三、總結(jié)
(1)實(shí)際開發(fā)中一般使用使用了Cache Aside Pattern緩存更新策略模式,此方案最大程度上保證了數(shù)據(jù)的一致性并且實(shí)現(xiàn)也最簡單
(2)無論是先操作數(shù)據(jù)庫再刪除緩存還是先刪除緩存再操作數(shù)據(jù)庫都有可能會出現(xiàn)刪除緩存失敗的情況,所以需要加入刪除重試機(jī)制
(3)如果想要Redis和Mysql的數(shù)據(jù)強(qiáng)一致性,可以考慮使用加鎖的方式實(shí)現(xiàn)
以上就是Redis與MySQL數(shù)據(jù)一致性問題的策略模式及解決方案的詳細(xì)內(nèi)容,更多關(guān)于Redis與MySQL數(shù)據(jù)一致性的資料請關(guān)注腳本之家其它相關(guān)文章!
- Redis和數(shù)據(jù)庫的一致性(Canal+MQ) 的實(shí)現(xiàn)
- 使用Canal實(shí)現(xiàn)MySQL數(shù)據(jù)同步的完整指南
- canal實(shí)現(xiàn)mysql數(shù)據(jù)同步的詳細(xì)過程
- 兩個windows服務(wù)器使用canal實(shí)現(xiàn)mysql實(shí)時同步
- Canal實(shí)現(xiàn)MYSQL實(shí)時數(shù)據(jù)同步的示例代碼
- Canal進(jìn)行MySQL到MySQL數(shù)據(jù)庫全量+增量同步踩坑指南
- 基于Docker結(jié)合Canal實(shí)現(xiàn)MySQL實(shí)時增量數(shù)據(jù)傳輸功能
- MySQL數(shù)據(jù)實(shí)時同步Redis的方案全解析
- 保證MySQL與Redis數(shù)據(jù)一致性的6種實(shí)現(xiàn)方案
- 詳解讓MySQL和Redis數(shù)據(jù)保持一致的四種策略
- Java使用Canal同步MySQL數(shù)據(jù)到Redis
- Linux寶塔面板使用Canal實(shí)現(xiàn)Mysql和Redis數(shù)據(jù)同步(圖文教程)
相關(guān)文章
Redis與數(shù)據(jù)庫數(shù)據(jù)一致性的原因及解決方案
Redis作為一種高效的鍵值對存儲系統(tǒng),常用于緩存數(shù)據(jù)庫減少IO操作,下面這篇文章主要介紹了Redis與數(shù)據(jù)庫數(shù)據(jù)一致性的原因及解決方案,文中介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04
利用Redis的有序集合實(shí)現(xiàn)排行榜功能實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于如何利用Redis的有序集合實(shí)現(xiàn)排行榜功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
Redis鎖的過期時間小于業(yè)務(wù)的執(zhí)行時間如何續(xù)期
本文主要介紹了Redis鎖的過期時間小于業(yè)務(wù)的執(zhí)行時間如何續(xù)期,Redisson它能給Redis分布式鎖實(shí)現(xiàn)過期時間自動續(xù)期,具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05
redis主從切換導(dǎo)致的數(shù)據(jù)丟失與陷入只讀狀態(tài)故障解決方案
這篇文章主要介紹了redis主從切換導(dǎo)致的數(shù)據(jù)丟失與陷入只讀狀態(tài)故障解決方案的相關(guān)資料,需要的朋友可以參考下2023-05-05
利用Redis實(shí)現(xiàn)訂單30分鐘自動取消
本文主要介紹了利用Redis實(shí)現(xiàn)訂單30分鐘自動取消,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
redis-copy使用6379端口無法連接到Redis服務(wù)器的問題
這篇文章主要介紹了redis-copy使用6379端口無法連接到Redis服務(wù)器的問題的相關(guān)資料,需要的朋友可以參考下2023-05-05

