Java中如何保證緩存一致性問題
前言:
一道之前的面試題:
如何保證緩存和數(shù)據(jù)庫的一致性?
下面介紹幾種方案(大家回答的時(shí)候最好根據(jù)自己的業(yè)務(wù),結(jié)合下面的方案)
方案分析
更新緩存策略方式常見的有下面幾種:
- 先更新緩存,再更新數(shù)據(jù)庫
- 先更新數(shù)據(jù)庫,再更新緩存
- 先刪除緩存,再更新數(shù)據(jù)庫
- 先更新數(shù)據(jù)庫,再刪除緩存
下面一一介紹!
方案一:更新緩存,更新數(shù)據(jù)庫
這種方式可輕易排除,因?yàn)槿绻雀戮彺娉晒Γ菙?shù)據(jù)庫更新失敗,則肯定會(huì)造成數(shù)據(jù)不一致。
方案二:更新數(shù)據(jù)庫,更新緩存
這種緩存更新策略俗稱雙寫,存在問題是:并發(fā)更新數(shù)據(jù)庫場(chǎng)景下,會(huì)將臟數(shù)據(jù)刷到緩存
updateDB(); updateRedis();
舉例:如果在兩個(gè)操作之間數(shù)據(jù)庫和緩存又被后面請(qǐng)求修改,此時(shí)再去更新緩存已經(jīng)是過期數(shù)據(jù)了。
方案三:刪除緩存,更新數(shù)據(jù)庫
存在問題:更新數(shù)據(jù)庫之前,若有查詢請(qǐng)求,會(huì)將臟數(shù)據(jù)刷到緩存
deleteRedis(); updateDB();
舉例:如果在兩個(gè)操作之間發(fā)生了數(shù)據(jù)查詢,那么會(huì)有舊數(shù)據(jù)放入緩存。
該方案會(huì)導(dǎo)致請(qǐng)求數(shù)據(jù)不一致
如果同時(shí)有一個(gè)請(qǐng)求A進(jìn)行更新操作,另一個(gè)請(qǐng)求B進(jìn)行查詢操作。那么會(huì)出現(xiàn)如下情形:
- 請(qǐng)求A進(jìn)行寫操作,刪除緩存
- 請(qǐng)求B查詢發(fā)現(xiàn)緩存不存在
- 請(qǐng)求B去數(shù)據(jù)庫查詢得到舊值
- 請(qǐng)求B將舊值寫入緩存
- 請(qǐng)求A將新值寫入數(shù)據(jù)庫
上述情況就會(huì)導(dǎo)致不一致的情形出現(xiàn)。而且,如果不采用給緩存設(shè)置過期時(shí)間策略,該數(shù)據(jù)永遠(yuǎn)都是臟數(shù)據(jù)。
方案四:更新數(shù)據(jù)庫,刪除緩存
存在問題:在更新數(shù)據(jù)庫之前有查詢請(qǐng)求,并且緩存失效了,會(huì)查詢數(shù)據(jù)庫,然后更新緩存。如果在查詢數(shù)據(jù)庫和更新緩存之間進(jìn)行了數(shù)據(jù)庫更新的操作,那么就會(huì)把臟數(shù)據(jù)刷到緩存
updateDB(); deleteRedis();
舉例:如果在查詢數(shù)據(jù)庫和放入緩存這兩個(gè)操作中間發(fā)生了數(shù)據(jù)更新并且刪除緩存,那么會(huì)有舊數(shù)據(jù)放入緩存。
假設(shè)有兩個(gè)請(qǐng)求,一個(gè)請(qǐng)求A做查詢操作,一個(gè)請(qǐng)求B做更新操作,那么會(huì)有如下情形產(chǎn)生
- 緩存剛好失效
- 請(qǐng)求A查詢數(shù)據(jù)庫,得一個(gè)舊值
- 請(qǐng)求B將新值寫入數(shù)據(jù)庫
- 請(qǐng)求B刪除緩存
- 請(qǐng)求A將查到的舊值寫入緩存
如果發(fā)生上述情況,確實(shí)是會(huì)發(fā)生臟數(shù)據(jù)。但是發(fā)生上述情況有一個(gè)先天性條件,就是寫數(shù)據(jù)庫操作比讀數(shù)據(jù)庫操作耗時(shí)更短
不過數(shù)據(jù)庫的讀操作的速度遠(yuǎn)快于寫操作的
因此這一情形很難出現(xiàn)。
方案對(duì)比
方案1和方案2的共同缺點(diǎn):
并發(fā)更新數(shù)據(jù)庫場(chǎng)景下,會(huì)將臟數(shù)據(jù)刷到緩存,但一般并發(fā)寫的場(chǎng)景概率都相對(duì)小一些;
線程安全角度,會(huì)產(chǎn)生臟數(shù)據(jù),比如:
- 線程A更新了數(shù)據(jù)庫
- 線程B更新了數(shù)據(jù)庫
- 線程B更新了緩存
- 線程A更新了緩存
方案3和方案4的共同缺點(diǎn):
不管采用哪種順序,2種方式都是存在一些問題的:
- 主從延時(shí)問題:不管是先刪除還是后刪除,數(shù)據(jù)庫主從延時(shí)可能導(dǎo)致臟數(shù)據(jù)的產(chǎn)生。
- 緩存刪除失?。喝绻彺鎰h除失敗,則都會(huì)產(chǎn)生臟數(shù)據(jù)。
問題解決思路:延遲雙刪,添加重試機(jī)制,下面介紹!
更新緩存還是刪除緩存?
- 1.更新緩存緩存需要有一定的維護(hù)成本,而且會(huì)存在并發(fā)更新的問題
- 2.寫多讀少的情況下,讀請(qǐng)求還沒有來,緩存以及被更新很多次,沒有起到緩存的作用
- 3.放入緩存的值可能是經(jīng)過復(fù)雜計(jì)算的,如果每次更新,都計(jì)算寫入緩存的值,浪費(fèi)性能的
刪除緩存優(yōu)點(diǎn):簡單、成本低,容易開發(fā);缺點(diǎn):會(huì)造成一次cache miss
如果更新緩存開銷較小并且讀多寫少,基本不會(huì)有寫并發(fā)的時(shí)候可以才用更新緩存,否則通用做法還是刪除緩存。
總結(jié)
方案 | 問題 | 問題出現(xiàn)概率 | 推薦程度 |
---|---|---|---|
更新緩存 -> 更新數(shù)據(jù)庫 | 為了保證數(shù)據(jù)準(zhǔn)確性,數(shù)據(jù)必須以數(shù)據(jù)庫更新結(jié)果為準(zhǔn),所以該方案絕不可行 | 大 | 不推薦 |
更新數(shù)據(jù)庫 -> 更新緩存 | 并發(fā)更新數(shù)據(jù)庫場(chǎng)景下,會(huì)將臟數(shù)據(jù)刷到緩存 | 并發(fā)寫場(chǎng)景,概率一般 | 寫請(qǐng)求較多時(shí)會(huì)出現(xiàn)不一致問題,不推薦使用。 |
刪除緩存 -> 更新數(shù)據(jù)庫 | 更新數(shù)據(jù)庫之前,若有查詢請(qǐng)求,會(huì)將臟數(shù)據(jù)刷到緩存 | 并發(fā)讀場(chǎng)景,概率較大 | 讀請(qǐng)求較多時(shí)會(huì)出現(xiàn)不一致問題,不推薦使用 |
更新數(shù)據(jù)庫 -> 刪除緩存 | 在更新數(shù)據(jù)庫之前有查詢請(qǐng)求,并且緩存失效了,會(huì)查詢數(shù)據(jù)庫,然后更新緩存。如果在查詢數(shù)據(jù)庫和更新緩存之間進(jìn)行了數(shù)據(jù)庫更新的操作,那么就會(huì)把臟數(shù)據(jù)刷到緩存 | 并發(fā)讀場(chǎng)景&讀操作慢于寫操作,概率最小 | 讀操作比寫操作更慢的情況較少,相比于其他方式出錯(cuò)的概率小一些。勉強(qiáng)推薦。 |
推薦方案
延遲雙刪
采用更新前后雙刪除緩存策略
public void write(String key,Object data){ redis.del(key); db.update(data); Thread.sleep(1000); redis.del(key); }
- 先淘汰緩存
- 再寫數(shù)據(jù)庫
- 休眠1秒,再次淘汰緩存
大家應(yīng)該評(píng)估自己的項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)。然后寫數(shù)據(jù)的休眠時(shí)間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)基礎(chǔ)上即可。
這么做的目的,就是確保讀請(qǐng)求結(jié)束,寫請(qǐng)求可以刪除讀請(qǐng)求造成的緩存臟數(shù)據(jù)。
問題及解法:
1、同步刪除,吞吐量降低如何處理
將第二次刪除作為異步的,提交一個(gè)延遲的執(zhí)行任務(wù)
2、解決刪除失敗的方式:
添加重試機(jī)制,例如:將刪除失敗的key,寫入消息隊(duì)列;但對(duì)業(yè)務(wù)耦合有些嚴(yán)重;
延時(shí)工具可以選擇:
最普通的阻塞Thread.currentThread().sleep(1000);
Jdk調(diào)度線程池,quartz定時(shí)任務(wù),利用jdk自帶的delayQueue,netty的HashWheelTimer,Rabbitmq的延時(shí)隊(duì)列,等等
實(shí)際場(chǎng)景
我們有個(gè)商品中心的場(chǎng)景,是讀多寫少的服務(wù),并且寫數(shù)據(jù)會(huì)發(fā)送MQ通知下游拿數(shù)據(jù),這樣就需要嚴(yán)格保證緩存和數(shù)據(jù)庫的一致性,需要提供高可靠的系統(tǒng)服務(wù)能力。
寫緩存策略
- 緩存key設(shè)置失效時(shí)間
- 先DB操作,再緩存失效
- 寫操作都標(biāo)記key(美團(tuán)中間件)強(qiáng)制走主庫
- 接入美團(tuán)中間件監(jiān)聽binlog(美團(tuán)中間件)變化的數(shù)據(jù)在進(jìn)行兜底,再刪除緩存
讀緩存策略
- 先判斷是否走主庫
- 如果走主庫,則使用標(biāo)記(美團(tuán)中間件)查主庫
- 如果不是,則查看緩存中是否有數(shù)據(jù)
- 緩存中有數(shù)據(jù),則使用緩存數(shù)據(jù)作為結(jié)果
- 如果沒有,則查DB數(shù)據(jù),再寫數(shù)據(jù)到緩存
注意
關(guān)于緩存過期時(shí)間的問題
如果緩存設(shè)置了過期時(shí)間,那么上述的所有不一致情況都只是暫時(shí)的。
但是如果沒有設(shè)置過期時(shí)間,那么不一致問題就只能等到下次更新數(shù)據(jù)時(shí)解決。
所以一定要設(shè)置緩存過期時(shí)間。
到此這篇關(guān)于Java緩存一致性問題的文章就介紹到這了,更多相關(guān)Java緩存一致性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java純代碼實(shí)現(xiàn)導(dǎo)出PDF功能
在項(xiàng)目開發(fā)中,產(chǎn)品的需求越來越奇葩啦,開始文件下載都是下載為excel的,做著做著需求竟然變了,要求能導(dǎo)出pdf,本文就來和大家分享一下Java實(shí)現(xiàn)導(dǎo)出PDF的常用方法吧2023-07-07java文字轉(zhuǎn)語音的實(shí)現(xiàn)示例
在Java中,我們可以使用第三方庫來實(shí)現(xiàn)文字轉(zhuǎn)語音的功能,本文主要介紹了java文字轉(zhuǎn)語音的實(shí)現(xiàn)示例,選擇jacob技術(shù)實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03java sql ResultSet 之getRow()用法說明
這篇文章主要介紹了java sql ResultSet 之getRow()用法說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08Java System.currentTimeMillis()時(shí)間的單位轉(zhuǎn)換與計(jì)算方式案例詳解
這篇文章主要介紹了Java System.currentTimeMillis()時(shí)間的單位轉(zhuǎn)換與計(jì)算方式案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08