1、緩存雙寫一致性的理解
如果redis中有數(shù)據(jù):需要和數(shù)據(jù)庫中的值相同
如果redis中無數(shù)據(jù):數(shù)據(jù)庫中的值要是最新值,且準(zhǔn)備回寫redis
緩存按照操作來分,可細(xì)分為兩種:只讀緩存和讀寫緩存
只讀緩存很簡單:就是Redis只做查詢,有就是有,沒有就是沒有,不會(huì)再進(jìn)一步訪問MySQL,不再需要會(huì)寫機(jī)制
大部分都是讀寫緩存,又分為兩種:
同步直寫策略
寫數(shù)據(jù)庫后也同步寫redis緩存,緩存和數(shù)據(jù)庫中的數(shù)據(jù)一 致
對于讀寫緩存來說,要想保證緩存和數(shù)據(jù)庫中的數(shù)據(jù)一致,就要采用同步直寫策略
比如特別重要的數(shù)據(jù)、熱點(diǎn)敏感數(shù)據(jù),例如充值后就需要立馬更新,及時(shí)生效
異步緩寫策略
正常業(yè)務(wù)運(yùn)行中,mysq|數(shù)據(jù)變動(dòng)了,但是可以在業(yè)務(wù)上容許出現(xiàn)一定時(shí)間后才作用于redis,比如倉庫、物流系統(tǒng)、積分變更
異常情況出現(xiàn)了,不得不將失敗的動(dòng)作重新修補(bǔ),有可能需要借助kafka或者RabbitMQ等消息中間件,實(shí)現(xiàn)重試重寫
業(yè)務(wù)流程寫得沒錯(cuò),對于QPS(每秒查詢率)小于1000可以使用,但是對于高并發(fā)場景不適于。比如在高并發(fā)場景下,許多線程幾乎同時(shí)(時(shí)間間隔得不那么開)查詢相同的值,由于redis中沒有,會(huì)全部直接打到MySQL上,MySQL可能就會(huì)被打宕機(jī),即使沒宕機(jī),這些線程又會(huì)進(jìn)行大量的回寫,而且回寫的還是同一個(gè)值。這里的本質(zhì)就是:查詢MySQL和回寫不是原子操作。
對于上述情況,需要采用雙檢測加鎖策略,類似于單例模式中的懶漢模式(可以采用雙檢測加鎖策略實(shí)現(xiàn))
多個(gè)線程同時(shí)去查詢數(shù)據(jù)庫的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢數(shù)據(jù)的請求上使用一個(gè)互斥鎖來鎖住它。
其他的線程走到這一步拿不到鎖就等著,等第一個(gè)線程查詢到了數(shù)據(jù),然后做緩存。
后面的線程進(jìn)來發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
2、數(shù)據(jù)庫和緩存一致性的幾種更新策略
目的:無論怎么操作,我們要達(dá)到最終一致性
給緩存設(shè)置過期時(shí)間,定期清理緩存并回寫,是保證最終一致性的解決方案。
我們可以對存入緩存的數(shù)據(jù)設(shè)置過期時(shí)間,所有的寫操作以數(shù)據(jù)庫為準(zhǔn),對緩存操作只是盡最大努力即可。也就是說如果數(shù)據(jù)庫寫成功,緩存更新失敗,那么只要到達(dá)過期時(shí)間,則后面的讀請求自然會(huì)從數(shù)據(jù)庫中讀取新值然后回填緩存,達(dá)到一致性,切記,要以mysql的數(shù)據(jù)庫寫入庫為準(zhǔn)。
可以停機(jī)的情況下
掛牌報(bào)錯(cuò),凌晨升級,溫馨提示,服務(wù)降級
單線程,這樣重量級的數(shù)據(jù)操作最好不要多線程
不可以停機(jī)的情況,則有4種更新策略
2.1 先更新數(shù)據(jù)庫,再更新緩存
異常問題1:
- 先更新mysql的某商品的庫存,當(dāng)前商品的庫存是100,更新為99個(gè)
- 先更新mysq|修改為99成功,然后更新redis
- 此時(shí)假設(shè)異常出現(xiàn),更新redis失敗了,這導(dǎo)致mysq|里面的庫存是99而redis里面的還是100
- 上述發(fā)生,會(huì)讓數(shù)據(jù)庫里面和緩存redis里面數(shù)據(jù)不一致,讀到redis臟數(shù)據(jù)
例如:正當(dāng)需要更新redis的數(shù)據(jù)時(shí),此時(shí)可能master主機(jī)宕機(jī),salve正在處于上位(成為新的master)的過程,沒時(shí)間來回應(yīng)更新redis的數(shù)據(jù),或者更新redis的操作丟失,這就會(huì)導(dǎo)致mysql更新成功,redis更新失敗,后續(xù)所有讀操作都讀到了臟數(shù)據(jù)
異常問題2:
A、B兩個(gè)線程同時(shí)發(fā)起調(diào)用(實(shí)際上可能會(huì)存在更多的線程)
正常邏輯
- 線程A更新mysql中的某個(gè)值,例如a,更新為100
- 線程A更新redis中的a為100
- 線程B更新mysql中的a,更新為80
- 線程B更新redis中的a為80
異常邏輯
多線程環(huán)境下,A、B兩個(gè)線程有快有慢,有前有后有并行
- 線程A更新mysql中的a,更新為100
- 線程B更新mysql中的a,更新為80
- 線程B更新完mysql,立刻回寫更新redis中的a為80
- 線程A更新redis中的a為100
最終結(jié)果,mysq|和redis數(shù)據(jù)不一 致
2.2 先更新緩存,再更新數(shù)據(jù)庫
從技術(shù)上可以做,但不太推薦,業(yè)務(wù)上一般把mysq|作為底單數(shù)據(jù)庫,保證最后解釋
異常問題
正常邏輯
A、B兩個(gè)線程同時(shí)發(fā)起調(diào)用(實(shí)際上可能會(huì)存在更多的線程)
- 線程A更新redis中的a為100
- 線程A更新mysql中的a為100
- 線程B更新redis中的a為80
- 線程B更新mysql中的a為80
異常邏輯
多線程環(huán)境下,A、B兩個(gè)線程有快有慢,有前有后有并行
- 線程A更新redis中的a為100
- 線程B更新redis中的a為80
- 線程B更新redis中的a為80
- 線程A更新mysql中的a為100
最終結(jié)果,mysq|和redis數(shù)據(jù)不一 致
2.3 先刪除緩存,再更新數(shù)據(jù)庫
異常問題,先刪除緩存,再更新數(shù)據(jù)庫(不進(jìn)行回寫),等到再次查詢時(shí),才進(jìn)行回寫操作
- A線程先成功刪除了redis里面的數(shù)據(jù),然后去更新mysq|,此時(shí)mysq|正在更新中,還沒有結(jié)束(比如網(wǎng)路延時(shí),或者還沒有commit)
- 線程B突然要來讀取redis緩存數(shù)據(jù),由于redis里面的數(shù)據(jù)是空的,線程B就需要去mysql當(dāng)中讀取數(shù)據(jù),此時(shí)數(shù)據(jù)還是舊值
- 線程B從mysql中讀取到舊值,由于redis中沒有緩存,線程B會(huì)進(jìn)行回寫操作,把舊值寫回redis(剛剛被A線程刪除的舊數(shù)據(jù)又被寫回進(jìn)redis)
- A線程終于將數(shù)據(jù)更新成功
- 后續(xù)的所有讀操作都是讀的臟數(shù)據(jù)
總結(jié):如果數(shù)據(jù)庫更新失敗或超時(shí)或返回不及時(shí),導(dǎo)致B線程請求訪問緩存時(shí)發(fā)現(xiàn)redis里面沒數(shù)據(jù),緩存缺失,B再去讀取mysq|時(shí),從數(shù)據(jù)庫中讀取到舊值,還寫回redis, 導(dǎo)致A白干了
如何解決上訴異常
采用延時(shí)雙刪策略
- 線程A先成功刪除redis緩存
- 線程A更新數(shù)據(jù)庫(更新可能還沒有完成)
- 線程A更新成功后,先sleep幾秒(這幾秒表示其他業(yè)務(wù)邏輯導(dǎo)致耗時(shí)延時(shí))
- 線程A再次刪除redis緩存
- 線程B的讀取+回寫一定是在線程A第二次刪除redis緩存之前
加上sleep的這段時(shí)間,就是為了讓線程B能夠先從數(shù)據(jù)庫讀取數(shù)據(jù),再把缺失的數(shù)據(jù)寫入緩存,然后,線程A再進(jìn)行刪除。所以,線程A sleep的時(shí)間,就需要大于線程B讀取數(shù)據(jù)再寫入緩存的時(shí)間這樣一來,其它線程讀取數(shù)據(jù)時(shí),會(huì)發(fā)現(xiàn)緩存缺失,所以會(huì)從數(shù)據(jù)庫中讀取最新值。因?yàn)檫@個(gè)方案會(huì)在第一次刪除緩存值后,延遲一段時(shí)間再次進(jìn)行刪除,所以我們也把它叫做"延遲雙刪"
這么做的目的,就是確保讀請求結(jié)束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。
線程A刪除完成之后,休眠的時(shí)間該如何確定呢?
方案1:
在業(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ǔ)上加百毫秒即可。
方案2:
新啟動(dòng)一個(gè)后臺(tái)監(jiān)控程序,比如WatchDog監(jiān)控程序
這種同步淘汰策略,吞吐量降低了怎么辦?
將第二次刪除作為異步操作,讓一個(gè)子線程進(jìn)行異步刪除,這樣,寫的請求就不用沉睡一段時(shí)間后了,再返回。這么做,加大吞吐量。
2.4 先更新數(shù)據(jù)庫,再刪除緩存
異常問題
- 線程A更新數(shù)據(jù)庫中的值
- 線程A還沒有來得及刪除緩存的值,此時(shí)線程B讀取緩存,讀取的是緩存舊值
- 線程A刪除緩存
- 其他線程進(jìn)行redis讀取操作,利用回寫策略,把緩存更新為最新
雖然會(huì)出現(xiàn)緩存刪除失敗或者來不及,導(dǎo)致請求再次訪問redis時(shí)緩存命中,讀取到的是緩存舊值的這種情況,但是這個(gè)方案是最穩(wěn)妥的
為了解決上訴問題,可以使用消息中間件,來保證數(shù)據(jù)的最終一致性
流程:
(1)更新數(shù)據(jù)庫數(shù)據(jù)
(2) 數(shù)據(jù)庫會(huì)將操作信息寫入binlog日志當(dāng)中
(3) 訂閱程序提取出所需要的數(shù)據(jù)以及key
(4) 另起一段非業(yè)務(wù)代碼,獲得該信息
(5)嘗試刪除緩存操作,發(fā)現(xiàn)刪除失敗
(6)將這些信息發(fā)送至消息隊(duì)列
(7)重新從消息隊(duì)列中獲得該數(shù)據(jù),重試操作
1、可以把要?jiǎng)h除的緩存值或者是要更新的數(shù)據(jù)庫值暫存到消息隊(duì)列中(例如使用Kafka/RabbitMQ等)。
2、當(dāng)程序沒有能夠成功地刪除緩存值或者是更新數(shù)據(jù)庫值時(shí),可以從消息隊(duì)列中重新讀取這些值,然后再次進(jìn)行刪除或更新。
3、如果能夠成功地刪除或更新,我們就要把這些值從消息隊(duì)列中去除,以免重復(fù)操作,此時(shí),我們也可以保證數(shù)據(jù)庫和緩存的數(shù)據(jù)一致了,否則還需要再次進(jìn)行重試
4、如果重試超過的一定次數(shù)后還是沒有成功,我們就需要向業(yè)務(wù)層發(fā)送報(bào)錯(cuò)信息了,通知運(yùn)維人員。文章來源:http://www.zghlxwxcb.cn/news/detail-415869.html
對于Redis和MySQL(或者其它數(shù)據(jù)庫),不能保證數(shù)據(jù)實(shí)時(shí)一致性,也就說無論哪一方進(jìn)行了修改,另一方都能立刻同步,因?yàn)槠渲羞€存在許多不確定因素,例如網(wǎng)絡(luò)延時(shí),因此我們需要保證的是最終一致性
文章來源地址http://www.zghlxwxcb.cn/news/detail-415869.html
到了這里,關(guān)于Redis緩存雙寫一致性的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!