redis分布式鎖解決緩存雙寫一致性
如何解決緩存雙寫問(wèn)題
只要涉及到緩存,那么緩存雙寫的問(wèn)題就避免不了,每一種情況下使用的方案也不相同,對(duì)于數(shù)據(jù)一致性要求不高的場(chǎng)景,我們可以使用延時(shí)雙刪等方案來(lái)實(shí)現(xiàn),而對(duì)于一致性要求很高的場(chǎng)景,在之前查找的資料都是基于隊(duì)列來(lái)實(shí)現(xiàn),也就是所有的請(qǐng)求都進(jìn)入一個(gè)隊(duì)列,但是實(shí)現(xiàn)起來(lái)相對(duì)來(lái)說(shuō)比較復(fù)雜。今天就使用分布式鎖來(lái)實(shí)現(xiàn)
業(yè)務(wù)背景-美食分享
1: 現(xiàn)在有一個(gè)很火的美食博主分享了一篇美食,此刻是很多人都會(huì)來(lái)查看,對(duì)于美食分享是典型的讀多寫少的場(chǎng)景,可以利用緩存
//根據(jù)id查詢美食信息 public GoodsVO loadGoodsInfoById(Long id) { //從redis中拿用戶信息 Object obj = redisTemplate.opsForValue().get(GOODS_KEY + id); if(obj == null) { //如果redis中不存在,就從數(shù)據(jù)庫(kù)中獲取 GoodsVO goods = loadGoodsFromDb(id); //將結(jié)果保存到redis中 redisTemplate.opsForValue().set(GOODS_KEY+id,JSONUtil.toJsonPrettyStr(goods)); return fileUser; } return JSONUtil.toBean(obj.toString(), GoodsVO.class); }
//編輯美食信息 public void modifyGoodsById(GoodsVO goodsVO) throws Exception{ //刪除緩存 redisTemplate.delete(GOODS_KEY + goodsVO.getId()); //更新用戶信息 goodsMapper.updateById(goodsVO); //刪除緩存 Thread.sleep(1000); redisTemplate.delete(GOODS_KEY + goodsVO.getId()); }
而對(duì)于更新數(shù)據(jù)的時(shí)候是先更新緩存還是先更新數(shù)據(jù)庫(kù)呢?
1:先更新緩存再更新數(shù)據(jù)庫(kù)
- 如果更新緩存成功,更新數(shù)據(jù)庫(kù)失敗,那么緩存中的數(shù)據(jù)就是臟數(shù)據(jù),與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致
2:先更新數(shù)據(jù)庫(kù)再更新緩存
- 更新數(shù)據(jù)庫(kù)成功,但是此時(shí)更新緩存失敗,那么緩存中的還是臟數(shù)據(jù),與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致
3:延時(shí)雙刪 - 先刪除緩存,再更新數(shù)據(jù)庫(kù),最后再刪除緩存
- 即使第一次刪除緩存失敗了,后續(xù)的更新數(shù)據(jù)庫(kù)操作也不會(huì)執(zhí)行,此時(shí)來(lái)讀取的線程發(fā)現(xiàn)緩存中沒(méi)有數(shù)據(jù),從數(shù)據(jù)庫(kù)讀取最新的。
- 第一次刪除緩存成功,更新數(shù)據(jù)庫(kù)失敗,此時(shí)來(lái)讀取的線程發(fā)現(xiàn)緩存中沒(méi)有數(shù)據(jù),從數(shù)據(jù)庫(kù)讀取最新的。
- 第一次刪除緩存成功,更新數(shù)據(jù)庫(kù)成功,第二次刪除緩存成功,那么來(lái)讀取的線程發(fā)現(xiàn)緩存中沒(méi)有數(shù)據(jù),從數(shù)據(jù)庫(kù)讀取最新的。
- 第一次刪除除緩存成功,更新數(shù)據(jù)庫(kù)成功,第二次刪除緩存失敗,這時(shí)候就會(huì)有問(wèn)題了
問(wèn)題一:Thread-A第一次刪除緩存成功,然后更新數(shù)據(jù),但是這時(shí)候不知道怎么了,可能是線程阻塞了或者其它原因,導(dǎo)致還沒(méi)有開(kāi)始更新數(shù)據(jù)庫(kù),這時(shí)候另外一個(gè)線程Thread-B來(lái)讀取數(shù)據(jù)了,讀取到數(shù)據(jù)之后將數(shù)據(jù)放到緩存中,這時(shí)候Thread-A才開(kāi)始更新數(shù)據(jù)庫(kù),但是Thread-A在第二次刪除緩存的時(shí)候失敗了,此時(shí)就導(dǎo)致緩存中的數(shù)據(jù)是之前的舊數(shù)據(jù),與數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致
問(wèn)題二:假設(shè)修改的時(shí)候都沒(méi)問(wèn)題,第二次刪除的緩存的時(shí)候都正常,這時(shí)候讀取數(shù)據(jù)的時(shí)候緩存里面肯定就沒(méi)有了,這時(shí)候就要從數(shù)據(jù)庫(kù)讀取,如果這時(shí)候一下子并發(fā)量很高,那么這些線程都要從數(shù)據(jù)庫(kù)中查詢,這時(shí)候數(shù)據(jù)庫(kù)都有可能直接掛掉
分布式鎖
查詢
//根據(jù)id查詢美食信息 public GoodsVO loadGoodsInfoById(Long id) { //從redis中拿用戶信息 Object obj = redisTemplate.opsForValue().get(GOODS_KEY + id); if(obj == null) { //如果redis中不存在,就從數(shù)據(jù)庫(kù)中獲取 GoodsVO goods = loadGoodsFromDb(id); return fileUser; } return JSONUtil.toBean(obj.toString(), GoodsVO.class); } //查詢緩存中沒(méi)有,這時(shí)候就要去數(shù)據(jù)庫(kù)中查詢 public String getGoodInfoFromDb(Long id) { //此時(shí)這里要加的鎖和 modifyGoodsById()方法加的應(yīng)該是同一把鎖,這樣才能保證雙寫一致性 boolean lock = redisLock.tryLock(GOODS_KEY+id); if(!lock) { //獲取鎖失敗,嘗試從緩存中獲取 Object obj = redisTemplate.opsForValue().get(GOODS_KEY + id); if(obj != null){ return JSONUtil.toBean(obj.toString(), GoodsVO.class); }else{ //直接返回一個(gè)操作頻繁的信息給前端 throw new GloableException("訪問(wèn)頻繁,請(qǐng)稍后再試"); } } try{ //獲取到鎖了 //嘗試從緩存中拿 Object obj = redisTemplate.opsForValue().get(GOODS_KEY + id); if(obj != null) { return JSONUtil.toBean(obj.toString(), GoodsVO.class); } //緩存中沒(méi)有,從MySql中拿 GoodsVO goods = goodsMapper.selectById(id); if(goods == null) { //為了解決緩存穿透的問(wèn)題 redisTemplate.opsForValue().set(GOODS_NULL_KEY + id); return null; } //將數(shù)據(jù)庫(kù)查到的數(shù)據(jù)放到緩存中 redisTemplate.opsForValue().set(GOODS_KEY+id,JSONUtil.toJsonPrettyStr(goods)); return goods; }finally{ //釋放鎖 redisLock.unLock(GOODS_KEY+goodsVO.getId()); } }
修改
//編輯美食信息 public void modifyGoodsById(GoodsVO goodsVO) throws Exception{ //嘗試獲取鎖 boolean lock = redisLock.tryLock(GOODS_KEY+goodsVO.getId()); if(!lock) { //直接返回一個(gè)操作頻繁的信息給前端 throw new GloableException("更新數(shù)據(jù)失敗,請(qǐng)稍后再試....."); } try{ //獲取到鎖了 //更新用戶信息 goodsMapper.updateById(goodsVO); //更新完之后,將數(shù)據(jù)庫(kù)最新的數(shù)據(jù)更新到緩存中 redisTemplate.opsForValue().set(GOODS_KEY+id,JSONUtil.toJsonPrettyStr(goodsVO)); }finally{ //釋放鎖 redisLock.unLock(GOODS_KEY+goodsVO.getId()); } }
現(xiàn)在第一次來(lái)訪問(wèn)數(shù)據(jù),返現(xiàn)Redis中沒(méi)有,這時(shí)候就會(huì)去MySql中查,但是只有獲取到鎖的線程才可以去數(shù)據(jù)庫(kù)中查,所以此時(shí)只有一個(gè)線程訪問(wèn)數(shù)據(jù)庫(kù),這時(shí)候即使有線程要去修改數(shù)據(jù),由于鎖已經(jīng)被拿走了,無(wú)法獲取到鎖,也就無(wú)法修改,保證了數(shù)據(jù)一致性。
假設(shè)現(xiàn)在修改的線程獲取到鎖了。由于之前Redis中已經(jīng)有數(shù)據(jù)了,此時(shí)所有讀取數(shù)據(jù)的線程都從Redis中拿,當(dāng)修改完數(shù)據(jù)之后,重新設(shè)置緩存,此時(shí)緩存中的數(shù)據(jù)就是最新的
以上就是分布式鎖解決緩存雙寫一致性的詳細(xì)內(nèi)容,更多關(guān)于分布式鎖解決緩存雙寫一致性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis優(yōu)惠券秒殺企業(yè)實(shí)戰(zhàn)
本文主要介紹了Redis優(yōu)惠券秒殺企業(yè)實(shí)戰(zhàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Redis Sentinel實(shí)現(xiàn)高可用配置的詳細(xì)步驟
這篇文章主要介紹了Redis Sentinel實(shí)現(xiàn)高可用配置的詳細(xì)步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09Redis中哈希結(jié)構(gòu)(Dict)的實(shí)現(xiàn)
本文主要介紹了Redis中哈希結(jié)構(gòu)(Dict)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06redis監(jiān)聽(tīng)key過(guò)期事件的詳細(xì)步驟
本文主要介紹了redis監(jiān)聽(tīng)key過(guò)期事件的詳細(xì)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Redis發(fā)布訂閱和實(shí)現(xiàn).NET客戶端詳解
發(fā)布訂閱在應(yīng)用級(jí)其作用是為了減少依賴關(guān)系,通常也叫觀察者模式。主要是把耦合點(diǎn)單獨(dú)抽離出來(lái)作為第三方,隔離易變化的發(fā)送方和接收方。下面這篇文章主要給大家介紹了關(guān)于Redis發(fā)布訂閱和實(shí)現(xiàn).NET客戶端的相關(guān)資料,需要的朋友可以參考下2017-03-03redis中hiredis-API函數(shù)的調(diào)用方法
這篇文章主要介紹了redis中hiredis-API函數(shù)的調(diào)用,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-09-09redis過(guò)期監(jiān)聽(tīng)機(jī)制方式
這篇文章主要介紹了redis過(guò)期監(jiān)聽(tīng)機(jī)制方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05