Redis與MySQL數(shù)據(jù)一致性問(wèn)題的策略模式及解決方案
在開(kāi)發(fā)中,一般會(huì)使用Redis緩存一些常用的熱點(diǎn)數(shù)據(jù)用來(lái)減少數(shù)據(jù)庫(kù)IO,提高系統(tǒng)的吞吐量
先了解一下分布式系統(tǒng)中的一致性概念。
強(qiáng)一致性:所有節(jié)點(diǎn)的數(shù)據(jù)必須實(shí)時(shí)同步,保證任何時(shí)候讀取到的數(shù)據(jù)都是最新的。
弱一致性:系統(tǒng)允許數(shù)據(jù)暫時(shí)不一致,但最終會(huì)達(dá)到一致?tīng)顟B(tài)。
最終一致性:數(shù)據(jù)更新后,經(jīng)過(guò)一段時(shí)間,系統(tǒng)會(huì)逐步達(dá)到一致?tīng)顟B(tài)。這個(gè)時(shí)間不固定,但在業(yè)務(wù)允許的范圍內(nèi)。
雙寫(xiě)一致性:當(dāng)數(shù)據(jù)同時(shí)存在于緩存(Redis)和數(shù)據(jù)庫(kù)(MySQL)時(shí),兩者之間數(shù)據(jù)一致
那么容易出現(xiàn)數(shù)據(jù)一致性問(wèn)題的場(chǎng)景是:
- 數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù),未更新緩存
- 刪除緩存后,數(shù)據(jù)庫(kù)更新失敗
一、策略模式
緩存可以提升性能、緩解數(shù)據(jù)庫(kù)壓力,但是使用緩存也會(huì)導(dǎo)致數(shù)據(jù)不一致性的問(wèn)題。有三種經(jīng)典的緩存使用模式:
- Cache-Aside Pattern
- Read-Through/Write-through
- Write-behind
1、旁路緩存模式(Cache Aside Pattern)
Cache Aside Pattern的提出是為了盡可能地解決緩存與數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致問(wèn)題
流程:
- 讀取操作:先從緩存中讀取數(shù)據(jù),緩存命中返回結(jié)果;緩存未命中,從DB中讀取數(shù)據(jù),并將數(shù)據(jù)寫(xiě)入緩存。
- 更新操作:先更DB,再刪除緩存中的舊數(shù)據(jù)。
在日常開(kāi)發(fā)中,一般使用了Cache Aside Pattern緩存更新策略模式,以數(shù)據(jù)庫(kù)為主,緩存為輔
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ù)庫(kù)獲取數(shù)據(jù) value = database.get(key); if (value != null) { // 將數(shù)據(jù)寫(xiě)入緩存 redis.set(key, value); } } return value; } // 更新操作 public void updateData(String key, String value) { // 更新數(shù)據(jù)庫(kù) database.update(key, value); // 刪除緩存中的舊數(shù)據(jù) redis.delete(key); } }
?:Cache-Aside在操作數(shù)據(jù)庫(kù)時(shí),為什么是先操作數(shù)據(jù)庫(kù)呢?為什么不先操作緩存呢?
1、先刪除緩存后,數(shù)據(jù)庫(kù)更新失敗
線程1:刪除緩存A,由于網(wǎng)絡(luò)問(wèn)題沒(méi)有操作數(shù)據(jù)庫(kù)失敗
線程2:查詢A,緩存無(wú)數(shù)據(jù),并把A寫(xiě)入緩存
線程1:網(wǎng)絡(luò)堵塞結(jié)束,修改數(shù)據(jù)庫(kù)A為B
那么此時(shí)緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫(kù)是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦?。?!
因此,Cache-Aside緩存模式,選擇了先操作數(shù)據(jù)庫(kù)而不是先操作緩存
2、先操作數(shù)據(jù)庫(kù)再刪除緩存方案
線程1:操作數(shù)據(jù)庫(kù),A更新數(shù)據(jù)為B,刪除緩存A
線程2:查詢A,緩存無(wú)數(shù)據(jù),并把B寫(xiě)入緩存
這種方案下,在數(shù)據(jù)庫(kù)更新成功后到刪除Redis緩存數(shù)據(jù)之前的這段時(shí)間中,其他線程讀取的數(shù)據(jù)都是舊數(shù)據(jù),等Redis刪除緩存后會(huì)重新從數(shù)據(jù)庫(kù)中讀取最新數(shù)據(jù)同步到Redis,這樣可以在一定程度上保證數(shù)據(jù)的最終一致性
但是在極端情況下,線程1的緩存刪除失敗,線程2讀取的也就是舊數(shù)據(jù)A,而不是新數(shù)據(jù)B了
這種方案也就是旁路緩存模式,那么Cache-Aside的優(yōu)缺點(diǎn)就是:
優(yōu)點(diǎn):
簡(jiǎn)單易懂,易于實(shí)現(xiàn)
讀性能高,因?yàn)榇蟛糠肿x操作都會(huì)命中緩存
缺點(diǎn):
更新數(shù)據(jù)庫(kù)后緩存可能還沒(méi)刪除,存在短暫的不一致
刪除緩存后,如果數(shù)據(jù)庫(kù)更新失敗,會(huì)導(dǎo)致數(shù)據(jù)不一致
?:Cache-Aside在寫(xiě)入請(qǐng)求的時(shí)候,為什么是刪除緩存而不是更新緩存呢?
線程1:操作數(shù)據(jù)庫(kù),更新數(shù)據(jù)為A,由于網(wǎng)絡(luò)問(wèn)題未更新緩存
線程2:操作數(shù)據(jù)庫(kù),更新數(shù)據(jù)為B,更新緩存為B
線程1:網(wǎng)絡(luò)堵塞結(jié)束,更新緩存為A
那么此時(shí)緩存是A【舊數(shù)據(jù)】,數(shù)據(jù)庫(kù)是B【新數(shù)據(jù)】,臟數(shù)據(jù)出現(xiàn)啦?。?!
如果是刪除緩存取代更新緩存則不會(huì)出現(xiàn)這個(gè)臟數(shù)據(jù)問(wèn)題?。?!
因此,Cache-Aside緩存模式,選擇了刪除緩存而不是更新緩存
適應(yīng)場(chǎng)景:適用于讀多寫(xiě)少的場(chǎng)景,特別是對(duì)數(shù)據(jù)一致性要求不是特別高的應(yīng)用
2、讀寫(xiě)穿透(Read-Through/Write-Through)
Read-Through:當(dāng)緩存未命中時(shí),自動(dòng)從數(shù)據(jù)庫(kù)加載數(shù)據(jù),并寫(xiě)入緩存
Write-Through:當(dāng)緩存更新時(shí),同步將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)
和旁路緩存模式很像,只有寫(xiě)操作不太一樣
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ù)庫(kù)獲取數(shù)據(jù) value = database.get(key); if (value != null) { // 將數(shù)據(jù)寫(xiě)入緩存 redis.set(key, value); } } return value; } // Write-Through public void writeThrough(String key, String value) { // 將數(shù)據(jù)寫(xiě)入緩存 redis.set(key, value); // 同步將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù) database.update(key, value); } }
優(yōu)點(diǎn):
- 保證了數(shù)據(jù)的強(qiáng)一致性,緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)始終同步。
- 讀寫(xiě)操作都由緩存處理,數(shù)據(jù)庫(kù)壓力較小。
缺點(diǎn):
- 寫(xiě)操作的延遲較高,因?yàn)?strong>每次寫(xiě)入緩存時(shí)都需要同步寫(xiě)入數(shù)據(jù)庫(kù),增加了系統(tǒng)的響應(yīng)時(shí)間
- 實(shí)現(xiàn)復(fù)雜度較高,需要額外的緩存同步機(jī)制
適應(yīng)場(chǎng)景:適合讀多寫(xiě)多、且對(duì)數(shù)據(jù)一致性要求較高的場(chǎng)景
3、異步緩存寫(xiě)入(Write Behind)
異步緩存就是緩存更新后,異步批量寫(xiě)入數(shù)據(jù)庫(kù)。這種策略適用于可以容忍一定數(shù)據(jù)不一致的高性能場(chǎng)景
示例代碼:
public class WriteBehindPattern { private RedisService redis; private DatabaseService database; private UpdateQueue updateQueue; // 異步緩存寫(xiě)入 public void writeBehind(String key, String value) { // 將數(shù)據(jù)寫(xiě)入緩存 redis.set(key, value); // 異步將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù) asyncDatabaseUpdate(key, value); } private void asyncDatabaseUpdate(String key, String value) { // 異步操作,將更新請(qǐng)求放入隊(duì)列 updateQueue.add(new UpdateTask(key, value)); } }
優(yōu)點(diǎn):
寫(xiě)操作的性能非常高,因?yàn)橹恍韪戮彺?,?shù)據(jù)庫(kù)更新是異步進(jìn)行的
適用于對(duì)寫(xiě)操作性能要求較高的場(chǎng)景
缺點(diǎn):
存在數(shù)據(jù)不一致的風(fēng)險(xiǎn),緩存更新后數(shù)據(jù)庫(kù)可能還未更新。
實(shí)現(xiàn)復(fù)雜度較高,需要處理異步操作中的異常和重試
適應(yīng)場(chǎng)景:大批量數(shù)據(jù)讀取,允許短期數(shù)據(jù)不一致,寫(xiě)密集型場(chǎng)景
二、一致性解決方案
緩存系統(tǒng)適用的場(chǎng)景就是非強(qiáng)一致性的場(chǎng)景,它屬于CAP中的AP
CAP理論,指的是在一個(gè)分布式系統(tǒng)中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區(qū)容錯(cuò)性),三者不可得兼。
沒(méi)辦法做到數(shù)據(jù)庫(kù)與緩存絕對(duì)的一致性,但通過(guò)一些方案優(yōu)化處理,是可以保證弱一致性,最終一致性的
1、緩存延遲雙刪
流程:
- 先刪除緩存
- 再更新數(shù)據(jù)庫(kù)
- 休眠一會(huì)(比如1秒),再次刪除緩存
但休眠的時(shí)間內(nèi),可能有臟數(shù)據(jù),且第二次刪除也可能失敗,導(dǎo)致的數(shù)據(jù)不一致問(wèn)題
延遲雙刪策略只能保證最終的一致性,不能保證強(qiáng)一致性。由于對(duì)Redis的操作和Mysql的操作不是原子性操作,所以如果想保證數(shù)據(jù)的強(qiáng)一致性就需要加鎖控制,如下圖所示
加鎖之后勢(shì)必會(huì)帶來(lái)系統(tǒng)的吞吐量的下降,所以需要衡量利弊來(lái)確定是否使用加鎖
方案優(yōu)化:刪除失敗就多刪除幾次呀,保證刪除緩存成功就可以了!
所以可以引入刪除緩存重試機(jī)制
2、刪除重試機(jī)制
刪除緩存失敗,則將這些key放入到消息隊(duì)列中,消費(fèi)消息隊(duì)列的消息,獲取要?jiǎng)h除的key,重試刪除緩存操作
3、讀取biglog異步刪除緩存
重試刪除緩存機(jī)制還可以吧,就是會(huì)造成好多業(yè)務(wù)代碼入侵。
方案優(yōu)化:通過(guò)數(shù)據(jù)庫(kù)的binlog來(lái)異步淘汰key
以MySQL為例,通過(guò)canal監(jiān)聽(tīng)binlog日志感知數(shù)據(jù)的變動(dòng)后,canal客戶端執(zhí)行刪除Redis緩存數(shù)據(jù),如果緩存數(shù)據(jù)刪除失敗那么發(fā)送一條MQ消息讓canal客戶端繼續(xù)執(zhí)行刪除操作,這樣可以保證數(shù)據(jù)的最終一致性,但是這樣也增加了系統(tǒng)的復(fù)雜性
三、總結(jié)
(1)實(shí)際開(kāi)發(fā)中一般使用使用了Cache Aside Pattern緩存更新策略模式,此方案最大程度上保證了數(shù)據(jù)的一致性并且實(shí)現(xiàn)也最簡(jiǎn)單
(2)無(wú)論是先操作數(shù)據(jù)庫(kù)再刪除緩存還是先刪除緩存再操作數(shù)據(jù)庫(kù)都有可能會(huì)出現(xiàn)刪除緩存失敗的情況,所以需要加入刪除重試機(jī)制
(3)如果想要Redis和Mysql的數(shù)據(jù)強(qiáng)一致性,可以考慮使用加鎖的方式實(shí)現(xiàn)
以上就是Redis與MySQL數(shù)據(jù)一致性問(wèn)題的策略模式及解決方案的詳細(xì)內(nèi)容,更多關(guān)于Redis與MySQL數(shù)據(jù)一致性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis源碼環(huán)境構(gòu)建過(guò)程詳解
這篇文章主要介紹了Redis源碼環(huán)境構(gòu)建過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07Redis實(shí)現(xiàn)分布式鎖的實(shí)例講解
在本篇文章里小編給大家整理了一篇關(guān)于Redis實(shí)現(xiàn)分布式鎖的實(shí)例講解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)參考下。2021-12-12Redis中統(tǒng)計(jì)各種數(shù)據(jù)大小的方法
這篇文章主要介紹了Redis中統(tǒng)計(jì)各種數(shù)據(jù)大小的方法,本文使用PHP實(shí)現(xiàn)統(tǒng)計(jì)Redis內(nèi)存占用比較大的鍵,需要的朋友可以參考下2015-03-03Redis的5種數(shù)據(jù)類(lèi)型與常用命令講解
今天小編就為大家分享一篇關(guān)于Redis的5種數(shù)據(jù)類(lèi)型與常用命令講解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03如何基于Session實(shí)現(xiàn)短信登錄功能
對(duì)比起Cookie,Session是存儲(chǔ)在服務(wù)器端的會(huì)話,相對(duì)安全,并且不像Cookie那樣有存儲(chǔ)長(zhǎng)度限制,下面這篇文章主要給大家介紹了關(guān)于如何基于Session實(shí)現(xiàn)短信登錄功能的相關(guān)資料,需要的朋友可以參考下2022-10-10Spring Boot中使用Redis常用數(shù)據(jù)格式API操作技巧
本文介紹了在Spring Boot中使用Redis的一些技巧和數(shù)據(jù)格式,通過(guò)配置Redis連接,可以連接到Redis數(shù)據(jù)庫(kù),結(jié)合實(shí)例代碼介紹的非常詳細(xì),需要的朋友參考下吧2024-03-03