背景:使用到緩存,無論是本地內(nèi)存做緩存還是使用?Redis?做緩存,那么就會存在數(shù)據(jù)同步的問題,因?yàn)榕渲眯畔⒕彺嬖趦?nèi)存中,而內(nèi)存時無法感知到數(shù)據(jù)在數(shù)據(jù)庫的修改。這樣就會造成數(shù)據(jù)庫中的數(shù)據(jù)與緩存中數(shù)據(jù)不一致的問題。
共有四種方案:
- 先更新數(shù)據(jù)庫,后更新緩存
- 先更新緩存,后更新數(shù)據(jù)庫
- 先刪除緩存,后更新數(shù)據(jù)庫
- 先更新數(shù)據(jù)庫,后刪除緩存
第一種和第二種方案,沒有人使用的,因?yàn)榈谝环N方案存在問題是:并發(fā)更新數(shù)據(jù)庫場景下,會將臟數(shù)據(jù)刷到緩存。
第二種方案存在的問題是:如果先更新緩存成功,但是數(shù)據(jù)庫更新失敗,則肯定會造成數(shù)據(jù)不一致。
目前主要用第三和第四種方案。
1、先刪除緩存,后更新數(shù)據(jù)庫
該方案也會出問題,此時來了兩個請求,請求?A(更新操作)?和請求?B(查詢操作)
- 請求A進(jìn)行寫操作,刪除緩存
- 請求B查詢發(fā)現(xiàn)緩存不存在
- 請求B去數(shù)據(jù)庫查詢得到舊值
- 請求B將舊值寫入緩存
- 請求A將新值寫入數(shù)據(jù)庫
上述情況就會導(dǎo)致不一致的情形出現(xiàn)。而且,如果不采用給緩存設(shè)置過期時間策略,該數(shù)據(jù)永遠(yuǎn)都是臟數(shù)據(jù)。
答案一:延時雙刪
最簡單的解決辦法延時雙刪
使用偽代碼如下:
?
public void write(String?key,Object?data){
Redis.delKey(key);
????????db.updateData(data);
Thread.sleep(1000);
Redis.delKey(key);
}
轉(zhuǎn)化為中文描述就是?(1)先淘汰緩存?(2)再寫數(shù)據(jù)庫(這兩步和原來一樣)?(3)休眠1秒,再次淘汰緩存,這么做,可以將1秒內(nèi)所造成的緩存臟數(shù)據(jù),再次刪除。確保讀請求結(jié)束,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。自行評估自己的項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時,寫數(shù)據(jù)的休眠時間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時基礎(chǔ)上,加幾百ms即可。
如果使用的是?Mysql?的讀寫分離的架構(gòu)的話,那么其實(shí)主從同步之間也會有時間差。
主從同步時間差
此時來了兩個請求,請求?A(更新操作)?和請求?B(查詢操作)
- 請求?A?更新操作,刪除了?Redis
- 請求主庫進(jìn)行更新操作,主庫與從庫進(jìn)行同步數(shù)據(jù)的操作
- 請?B?查詢操作,發(fā)現(xiàn)?Redis?中沒有數(shù)據(jù)
- 去從庫中拿去數(shù)據(jù)
- 此時同步數(shù)據(jù)還未完成,拿到的數(shù)據(jù)是舊數(shù)據(jù)
此時的解決辦法就是如果是對?Redis?進(jìn)行填充數(shù)據(jù)的查詢數(shù)據(jù)庫操作,那么就強(qiáng)制將其指向主庫進(jìn)行查詢。
從主庫中拿數(shù)據(jù)
答案二:?更新與讀取操作進(jìn)行異步串行化
異步串行化
我在系統(tǒng)內(nèi)部維護(hù)n個內(nèi)存隊(duì)列,更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標(biāo)識,將該操作路由之后,發(fā)送到其中一個jvm內(nèi)部的內(nèi)存隊(duì)列中(對同一數(shù)據(jù)的請求發(fā)送到同一個隊(duì)列)。讀取數(shù)據(jù)的時候,如果發(fā)現(xiàn)數(shù)據(jù)不在緩存中,并且此時隊(duì)列里有更新庫存的操作,那么將重新讀取數(shù)據(jù)+更新緩存的操作,根據(jù)唯一標(biāo)識路由之后,也將發(fā)送到同一個jvm內(nèi)部的內(nèi)存隊(duì)列中。然后每個隊(duì)列對應(yīng)一個工作線程,每個工作線程串行地拿到對應(yīng)的操作,然后一條一條的執(zhí)行。
這樣的話,一個數(shù)據(jù)變更的操作,先執(zhí)行刪除緩存,然后再去更新數(shù)據(jù)庫,但是還沒完成更新的時候,如果此時一個讀請求過來,讀到了空的緩存,那么可以先將緩存更新的請求發(fā)送到隊(duì)列中,此時會在隊(duì)列中積壓,排在剛才更新庫的操作之后,然后同步等待緩存更新完成,再讀庫。
讀操作去重
多個讀庫更新緩存的請求串在同一個隊(duì)列中是沒意義的,因此可以做過濾,如果發(fā)現(xiàn)隊(duì)列中已經(jīng)有了該數(shù)據(jù)的更新緩存的請求了,那么就不用再放進(jìn)去了,直接等待前面的更新操作請求完成即可,待那個隊(duì)列對應(yīng)的工作線程完成了上一個操作(數(shù)據(jù)庫的修改)之后,才會去執(zhí)行下一個操作(讀庫更新緩存),此時會從數(shù)據(jù)庫中讀取最新的值,然后寫入緩存中。
如果請求還在等待時間范圍內(nèi),不斷輪詢發(fā)現(xiàn)可以取到值了,那么就直接返回;如果請求等待的時間超過一定時長,那么這一次直接從數(shù)據(jù)庫中讀取當(dāng)前的舊值。(返回舊值不是又導(dǎo)致緩存和數(shù)據(jù)庫不一致了么?那至少可以減少這個情況發(fā)生,因?yàn)榈却瑫r也不是每次都是,幾率很小吧。這里我想的是,如果超時了就直接讀舊值,這時候僅僅是讀庫后返回而不放緩存)
2、先更新數(shù)據(jù)庫,后刪除緩存
這一種情況也會出現(xiàn)問題,比如更新數(shù)據(jù)庫成功了,但是在刪除緩存的階段出錯了沒有刪除成功,那么此時再讀取緩存的時候每次都是錯誤的數(shù)據(jù)了。
此時解決方案就是利用消息隊(duì)列進(jìn)行刪除的補(bǔ)償。具體的業(yè)務(wù)邏輯用語言描述如下:
- 請求?A?先對數(shù)據(jù)庫進(jìn)行更新操作
- 在對?Redis?進(jìn)行刪除操作的時候發(fā)現(xiàn)報錯,刪除失敗
- 此時將Redis?的?key?作為消息體發(fā)送到消息隊(duì)列中
- 系統(tǒng)接收到消息隊(duì)列發(fā)送的消息后再次對?Redis?進(jìn)行刪除操作
但是這個方案會有一個缺點(diǎn)就是會對業(yè)務(wù)代碼造成大量的侵入,深深的耦合在一起,所以這時會有一個優(yōu)化的方案,我們知道對?Mysql?數(shù)據(jù)庫更新操作后再?binlog?日志中我們都能夠找到相應(yīng)的操作,那么我們可以訂閱?Mysql?數(shù)據(jù)庫的?binlog?日志對緩存進(jìn)行操作。
文章來源:http://www.zghlxwxcb.cn/news/detail-820570.html
利用訂閱?binlog?刪除緩存文章來源地址http://www.zghlxwxcb.cn/news/detail-820570.html
到了這里,關(guān)于如何保證緩存與數(shù)據(jù)庫雙寫時的數(shù)據(jù)一致性?的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!