專門做活動的網(wǎng)站怎么注冊一個(gè)自己的網(wǎng)站
Redis緩存雙寫一致性是指在將數(shù)據(jù)同時(shí)寫入緩存(如Redis)和數(shù)據(jù)庫(如MySQL)時(shí),確保兩者中的數(shù)據(jù)保持一致性。在分布式系統(tǒng)中,緩存通常用于提高數(shù)據(jù)讀取的速度和減輕數(shù)據(jù)庫的壓力。然而,當(dāng)數(shù)據(jù)更新時(shí),如果沒有適當(dāng)?shù)臋C(jī)制來同步緩存和數(shù)據(jù)庫,可能會導(dǎo)致用戶讀到的數(shù)據(jù)是過時(shí)的或不一致的。
1.緩存雙寫一致性的理解
如果redis中有數(shù)據(jù),需要和數(shù)據(jù)庫中的值相同
如果redis中無數(shù)據(jù),?數(shù)據(jù)庫中的值要是最新值,且準(zhǔn)備回寫redis
緩存按照操作來分,細(xì)分以下2種
-
只讀緩存
-
讀寫緩存
-
同步直寫策略:寫數(shù)據(jù)庫之后也同步寫redis緩存,緩存和數(shù)據(jù)庫中的數(shù)據(jù)一致;
對于讀寫緩存來說,要想保證緩存和數(shù)據(jù)庫中的數(shù)據(jù)一致,就要采用同步直寫策略
-
異步緩寫策略:正常業(yè)務(wù)中,MySQL數(shù)據(jù)變了,但是可以在業(yè)務(wù)上容許出現(xiàn)一定時(shí)間后才作用于redis,比如倉庫、物流系統(tǒng);異常情況出現(xiàn)了, 不得不將失敗的動作重新修補(bǔ),有可能需要借助kafka或者RabbitMQ等消息中間件,實(shí)現(xiàn)重試重寫
-
采用雙檢加鎖策略
多個(gè)線程同時(shí)去查詢數(shù)據(jù)庫的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢數(shù)據(jù)的請求上使用一個(gè)互斥鎖來鎖住它。其他的線程走到這一步拿不到鎖就等著,等第一個(gè)線程查詢到了數(shù)據(jù),然后做緩存。后面的線程進(jìn)來發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
如果數(shù)據(jù)庫里沒有,緩存空數(shù)據(jù),避免大量請求發(fā)生緩存穿透
2.數(shù)據(jù)庫和緩存一致性的更新策略
目的:達(dá)到數(shù)據(jù)最終一致性
? ?給緩存設(shè)置過期時(shí)間,定期清理緩存并回寫,是保證最終一致性的解決方案。 我們可以對存入緩存的數(shù)據(jù)設(shè)置過期時(shí)間,所有的寫操作以數(shù)據(jù)庫為準(zhǔn),對緩存操作只是盡最大努力即可。也就是說如果數(shù)據(jù)庫寫成功,緩存更新失敗,那么只要到達(dá)過期時(shí)間,則后面的讀請求自然會從數(shù)據(jù)庫中讀取新值然后回填緩存,達(dá)到一致性,切記,要以mysql的數(shù)據(jù)庫寫入庫為準(zhǔn)。
上述方案和后續(xù)落地案例是調(diào)研后的主流+成熟的做法,但是考慮到各個(gè)公司業(yè)務(wù)系統(tǒng)的差距,不是100%絕對正確,不保證絕對適配全部情況,需要自己酌情選擇打法,合適自己的最好。
可以停機(jī)的情況
掛牌報(bào)錯(cuò),凌晨升級,溫馨提示,服務(wù)降級
單線程,這樣重量級的數(shù)據(jù)操作最好不要多線程
我們討論4種更新策略
2.1?先更新數(shù)據(jù)庫,在更新緩存(不可取)
異常問題1
- 先更新mysql的某商品的庫存,當(dāng)前商品的庫存是100,更新為99個(gè)。
- 先更新mysql修改為99成功,然后更新redis。
- 此時(shí)假設(shè)異常出現(xiàn),更新redis失敗了,這導(dǎo)致mysql里面的庫存是99而redis里面的還是100。
- 上述發(fā)生,會讓數(shù)據(jù)庫里面和緩存redis里面數(shù)據(jù)不一致,讀到redis臟數(shù)據(jù)
異常問題2
【先更新數(shù)據(jù)庫,再更新緩存】﹐A、B兩個(gè)線程發(fā)起調(diào)用
【正常邏輯】
1 A update mysql 100
2 A update redis 100
3 B update mysql 80
4 B update redis 80
=============================
【異常邏輯】
多線程環(huán)境下,A、B兩個(gè)線程有快有慢,有前有后有并行
1 A update mysql 100
3 B update mysql 80
4 B update redis 80
2 A update redis 100
=============================
最終結(jié)果,mysql和lredis數(shù)據(jù)不一致
mysql:80,redis:100
2.2?先更新緩存,再更新數(shù)據(jù)庫(不可取)
不推薦,業(yè)務(wù)上一般把MySQL作為底單數(shù)據(jù)庫?,保證最后解釋
[先更新緩存,再更新數(shù)據(jù)庫],A、B兩個(gè)線程發(fā)起調(diào)用
[正常邏輯]
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
====================================
[異常邏輯]多線程環(huán)境下,A. B兩個(gè)線程有快有慢有并行
A update redis 100
B update redis 80
B update mysq| 80
A update mysql 100
====================================
mysql:100,redis:80
2.3?先刪除緩存,在更新數(shù)據(jù)庫(不可取)
異常問題:
步驟分析,先刪除緩存,再更新數(shù)據(jù)庫
1 A線程先成功刪除了redis里面的數(shù)據(jù),然后去更新mysql,此時(shí)mysql正在更新中,還沒有結(jié)束。(比如網(wǎng)絡(luò)延時(shí))
B突然出現(xiàn)要來讀取緩存數(shù)據(jù)。2 此時(shí)redis里面的數(shù)據(jù)是空的,B線程來讀取,先去讀redis里數(shù)據(jù)(已經(jīng)被A線程delete掉了),此處出來2個(gè)問題:
2.1 B從mysq|獲得了舊值
B線程發(fā)現(xiàn)redis里沒有(緩存缺失)馬上去mysql里面讀取,從數(shù)據(jù)庫里面讀取來的是舊值。
2.2 B會把獲得的舊值寫回redis
獲得舊值數(shù)據(jù)后返回前臺并回寫進(jìn)redis(剛被A線程刪除的舊數(shù)據(jù)有極大可能早被寫回了)。3 A線程更新完mysql,發(fā)現(xiàn)redis里面的緩存是臟數(shù)據(jù),A線程直接懵逼了,o(T_ .τ)o兩個(gè)并發(fā)操作,一個(gè)是更新操作,另一個(gè)是查詢操作,A刪除緩存后,B查詢操作沒有命中緩存,B先把老數(shù)據(jù)讀出來后放到緩存中,然后A更新操作更新了數(shù)據(jù)庫。
于是,在緩存中的數(shù)據(jù)還是老的數(shù)據(jù),導(dǎo)致緩存中的數(shù)據(jù)是臟的,而且還一直這樣臟下去了。
4總結(jié)流程:
(1)請求A進(jìn)行寫操作,刪除redis緩存后,工作正在進(jìn)行中,更新mysql.....
A還么有徹底更新完mysql,還沒commit
(2)請求B開工查詢,查詢r(jià)edis發(fā)現(xiàn)緩存不存在(被A從redis中刪除了)
(3)請求B繼續(xù),去數(shù)據(jù)庫查詢得到了mysq中的舊值(A還沒有更新完)
(4)請求B將舊值寫回redis緩存
(5)請求A將新值寫入mysql數(shù)據(jù)庫
上述情況就會導(dǎo)致不一致的情形出現(xiàn)。
先刪除緩存,再更新數(shù)據(jù)庫:如果數(shù)據(jù)庫更新失敗或超時(shí)或返回不及時(shí),導(dǎo)致B線程請求訪問緩存時(shí)發(fā)現(xiàn)redis里面沒數(shù)據(jù),緩存缺失,B再去讀取mysql時(shí),從數(shù)據(jù)庫中讀取到舊值,還寫回redis, 導(dǎo)致A白干了
解決方案:采用延時(shí)雙刪策略?
加上sleep的這段時(shí)間,就是為了讓線程B能夠先從數(shù)據(jù)庫讀取數(shù)據(jù),再把缺失的數(shù)據(jù)寫入緩存,然后,線程A再進(jìn)行刪除。所以,線程A sleep的時(shí)間,就需要大于線程B讀取數(shù)據(jù)再寫入緩存的時(shí)間。這樣一來,其它線程讀取數(shù)據(jù)時(shí),會發(fā)現(xiàn)緩存缺失,所以會從數(shù)據(jù)庫中讀取最新值。因?yàn)檫@個(gè)方案會在第一次刪除緩存值后,延遲一段時(shí)間再次進(jìn)行刪除,所以我們也把它叫做“延遲雙刪”。
延遲雙刪面試題
這個(gè)刪除該休眠多久呢?線程A sleep的時(shí)間,就需要大于線程B讀取數(shù)據(jù)再寫入緩存的時(shí)間。
這個(gè)時(shí)間怎么確定呢? 第一種方法: 在業(yè)務(wù)程序運(yùn)行的時(shí)候,統(tǒng)計(jì)下線程讀數(shù)據(jù)和寫緩存的操作時(shí)間,自行評估自己的項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí),以此為基礎(chǔ)來進(jìn)行估算。然后寫數(shù)據(jù)的休眠時(shí)間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)基礎(chǔ)上加百毫秒即可。 這么做的目的,就是確保讀請求結(jié)束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。 第二種方法: 新啟動一個(gè)后臺監(jiān)控程序,比如后面要講解的WatchDog監(jiān)控程序,會加時(shí)
這種同步淘汰策略,吞吐量降低怎么辦?
2.4?先更新數(shù)據(jù)庫,再刪除緩存
異常問題
時(shí)間 | 線程A | 線程B | 出現(xiàn)的問題 |
---|---|---|---|
t1 | 更新數(shù)據(jù)庫中的值...... | ||
t2 | 緩存立刻命中,此時(shí)B讀取的是緩存舊值 | A還沒來得及刪除緩存的值,導(dǎo)致B緩存命中讀到舊值 | |
t3 | 更新緩存的數(shù)據(jù),over |
先更新數(shù)據(jù)庫,在刪除緩存,假如緩存刪除失敗或者來不及刪除,導(dǎo)致請求再次訪問redis時(shí)緩存命中,讀取到的是緩存的舊值。
解決方案 :
- 可以把要?jiǎng)h除的緩存值或者是要更新的數(shù)據(jù)庫值暫存到消息隊(duì)列中(例如使用Kafka/RabbitMQ等)。
- 當(dāng)程序沒有能夠成功地刪除緩存值或者是更新數(shù)據(jù)庫值時(shí),可以從消息隊(duì)列中重新讀取這些值,然后再次進(jìn)行刪除或更新。
- 如果能夠成功地刪除或更新,我們就要把這些值從消息隊(duì)列中去除,以免重復(fù)操作,此時(shí),我們也可以保證數(shù)據(jù)庫和緩存的數(shù)據(jù)一致了,否則還需要再次進(jìn)行重試 4 如果重試超過的一定次數(shù)后還是沒有成功,我們就需要向業(yè)務(wù)層發(fā)送報(bào)錯(cuò)信息了,通知運(yùn)維人員。
3. 雙寫一致總結(jié)
方案如何選擇?利弊如何
在大多數(shù)業(yè)務(wù)場景下, 個(gè)人建議是,優(yōu)先使用先更新數(shù)據(jù)庫,再刪除緩存的方案(先更庫→后刪存)。理由如下:
1先刪除緩存值再更新數(shù)據(jù)庫,有可能導(dǎo)致請求因緩存缺失而訪問數(shù)據(jù)庫,給數(shù)據(jù)庫帶來壓力導(dǎo)致打滿mysql。
2如果業(yè)務(wù)應(yīng)用中讀取數(shù)據(jù)庫和寫緩存的時(shí)間不好估算,那么,延遲雙刪中的等待時(shí)間就不好設(shè)置。
多補(bǔ)充一句:如果使用先更新數(shù)據(jù)庫,再刪除緩存的方案)
如果業(yè)務(wù)層要求必須讀取一致性的數(shù)據(jù),那么我們就需要在更新數(shù)據(jù)庫時(shí),先在Redis緩存客戶端暫停并發(fā)讀請求,等數(shù)據(jù)庫更新完、緩存值刪除后,再讀取數(shù)據(jù),從而保證數(shù)據(jù)一致性,這是理論可以達(dá)到的效果,但實(shí)際,不推薦,因?yàn)檎鎸?shí)生產(chǎn)環(huán)境中,分布式下很難做到實(shí)時(shí)一致性,一般都是最終一致性。
策略 | 高并發(fā)多線程條件下 | 問題 | 現(xiàn)象 | 解決方案 |
---|---|---|---|---|
先刪除redis緩存,再更新mysql | 無 | 緩存刪除成功但數(shù)據(jù)庫更新失敗 | Java程序從數(shù)據(jù)庫中讀到舊值 | 再次更新數(shù)據(jù)庫,重試 |
有 | 緩存刪除成功但數(shù)據(jù)庫更新中... 有并發(fā)請求 | 并發(fā)請求從數(shù)據(jù)庫讀到舊值并回寫到redis,導(dǎo)致后續(xù)都是從redis讀取到舊值 | 再次刪除緩存,重試 | |
先更新mysql,再刪除redis緩存 | 無 | 數(shù)據(jù)庫更新成功,但緩存刪除失敗 | Java程序從redis中讀到舊值 | 再次刪除緩存,重試 |
有 | 數(shù)據(jù)庫更新成功但緩存刪除中...... 有并發(fā)讀請求 | 并發(fā)請求從緩存讀到舊值 | 等待redis刪除完成,這段時(shí)間數(shù)據(jù)不一致,短暫存在。 |
4.最后
祝愿大家國慶快樂!
感謝大家,請大家多多支持!