Redis緩存實(shí)例超詳細(xì)講解
1 前言
1.1 什么是緩存
緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache [ kæ? ] ),是存貯數(shù)據(jù)的臨時地方,一般讀寫性能較高。
緩存有很多中實(shí)現(xiàn)場景:對于web開發(fā),常見的有如下幾種:
而我們的Redis緩存功能就是屬于在應(yīng)用層緩存 。
1.2 緩存的作用及成本
作用:毫無疑問,就是提高讀寫的效率,有效降低后端服務(wù)器的負(fù)載,有效降低響應(yīng)時間。
成本:任何東西都有兩面性,緩存在帶來高效的讀寫效率的同時,也有著對應(yīng)的從成本。
比如:數(shù)據(jù)一致性成本、代碼維護(hù)成本、運(yùn)維成本等。
1.3 Redis緩存模型
如下圖
原本的模型應(yīng)該是客戶端發(fā)送請求給數(shù)據(jù)庫,數(shù)據(jù)庫返回?cái)?shù)據(jù)給客戶端,而Reids的緩存模型就是在原有的基礎(chǔ)上,在中間加上一層Redis(經(jīng)典的中間件思想~)用戶每次都會先去redis中查找數(shù)據(jù),如果未命中才會去數(shù)據(jù)庫中查找數(shù)據(jù),并寫入Reis當(dāng)中,這么一來,用于下次需要相同的數(shù)據(jù)的時候,就可以在Reis當(dāng)中進(jìn)行獲取,又因?yàn)镽edis的高讀寫效率,實(shí)現(xiàn)了緩存的效果~
2 給商戶信息添加緩存
基于上述的Redis緩存模型,我們可以得出下面的緩存添加邏輯:
代碼實(shí)現(xiàn):(直接看Service層實(shí)現(xiàn))
每次查詢到商品用戶信息后,添加緩存。
package com.hmdp.service.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY; import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> * */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryShopById(Long id) { //1.去redis中查詢商品是否存在 String key = CACHE_SHOP_KEY+id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ //2.存在,直接返回給用戶 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //3.不存在,帶著id去數(shù)據(jù)庫查詢是否存在商品 Shop shop = getById(id); if (shop == null){ //4.不存在,返回錯誤信息 Result.fail("商品信息不存在!"); } //5.存在,存入redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop)); //6.返回商品信息 return Result.ok(shop); } }
3 緩存更新策略
3.1 更新策略介紹
使用Redis做緩存時,緩存還是要及時更新的,不然就會出現(xiàn)數(shù)據(jù)庫和緩存數(shù)據(jù)不一致的情況,也是我們常說的Redis成本——數(shù)據(jù)一致性成本
緩存大致有以下三種更新策略:
1. 內(nèi)存淘汰策略:這種策略沒有維護(hù)成本,這是利用Redis的內(nèi)存淘汰機(jī)制,當(dāng)內(nèi)存不足的時候自動淘汰,下次請求時再繼續(xù)存入數(shù)據(jù)。
這種策略模型優(yōu)點(diǎn)在于沒有維護(hù)成本,但是內(nèi)存不足這種無法預(yù)定的情況就導(dǎo)致了緩存中會有很多舊的數(shù)據(jù),數(shù)據(jù)一致性差。
2. 超時剔除:這種策略就是比較實(shí)用的,就是給緩存添加TTL存活時間,下次查詢是更新緩存。
這種策略數(shù)據(jù)一致性一般,維護(hù)成本有但是較低,一般用于兜底方案~
3.主動更新策略:就是我們程序員手動的進(jìn)行數(shù)據(jù)庫和緩存之間的更新,但數(shù)據(jù)庫更新時,緩存也進(jìn)行更新。
這種策略數(shù)據(jù)一致性就是最高的(畢竟自己動手,豐衣足食),但同時維護(hù)成本也是最高的。
那么,又該如何選擇緩存更新策略呢?
我的覺得應(yīng)該是根據(jù)業(yè)務(wù)場景不同來選擇不同的更新策略:
當(dāng)數(shù)據(jù)一致性要求低時:l使用內(nèi)存淘汰機(jī)制。例如店鋪類型的查詢緩存。
當(dāng)有高一致性需求:使用主動更新,并以超時剔除作為兜底方案。例如店鋪詳情查詢的緩存
3.2 主動更新策略
上述提到的主動更新策略,無疑是維護(hù)成本最高的,但具體又有哪些維護(hù)方式呢?怎么去做主動更新維護(hù)呢?
如下圖:主動更新主要分為下面三種:
又因?yàn)楹髢煞N實(shí)現(xiàn)方式過于復(fù)雜,所以重點(diǎn)說第一種。
第一種:Cache Aside Pattern,由緩存調(diào)用者進(jìn)行操作,就是在我們數(shù)據(jù)庫進(jìn)行更新時,對緩存也進(jìn)行更新。
這又引出了好幾個問題了~
1. 緩存更新?是更新緩存還是直接刪除緩存?
2. 如何保證數(shù)據(jù)庫更新和緩存更新同時成功或失?。?/p>
3. 操作時應(yīng)該先操作緩存還是先操作數(shù)據(jù)庫?
第一個問題:
我想說,緩存更新如果是數(shù)據(jù)更新的話,每次更新數(shù)據(jù)庫都要對緩存數(shù)據(jù)進(jìn)行更新,有太多無效的讀寫操作,所以操作緩存時,選擇刪除緩存~
第二個問題:
要做到兩個操作一致性,第一想到的就應(yīng)該是事務(wù)。
解決方案:當(dāng)我們是單體系統(tǒng)時,將緩存和數(shù)據(jù)庫操作放在同一個事務(wù)里。
當(dāng)我們是分布式系統(tǒng)時,利用TTC等分布式事務(wù)方案
最后一個問題:先操作數(shù)據(jù)庫還是先操作緩存?
就只有下面兩種情況:
1.先刪除緩存,再操作數(shù)據(jù)庫
2.先操作數(shù)據(jù)庫,再刪除緩存
我們可以兩種情況都來看看~
第一種情況,先刪除緩存的情況,我們想的正常的不會出現(xiàn)問題的操作流程(左邊)和操作會出現(xiàn)問題的流程(右邊)
補(bǔ)充一下:這邊兩個線程最初的數(shù)據(jù)庫和緩存數(shù)據(jù)都假定為10~
出現(xiàn)問題的情況為我們線程1先刪除緩存,線程2未命中緩存,直接查到數(shù)據(jù)庫中數(shù)據(jù)為10并寫入緩存中,而線程1在線程2后寫入緩存后,把數(shù)據(jù)庫更新成了20,這樣最后數(shù)據(jù)庫數(shù)據(jù)為20,而緩存中的數(shù)據(jù)為10,這就導(dǎo)致數(shù)據(jù)不一致了。
2.先操作數(shù)據(jù)庫的情況
正確情況(左邊)錯誤情況(右邊),數(shù)據(jù)庫和緩存最初數(shù)據(jù)也都還是10~
第二種先操作數(shù)據(jù)庫的方式,它出現(xiàn)的錯誤情況主要是,線程1線程查詢了緩存,未命中后去查詢數(shù)據(jù)庫的同時,線程2更新了數(shù)據(jù)庫,并且刪除了緩存,然后線程1才把數(shù)據(jù)庫中查出來的10寫入緩存,導(dǎo)致緩存為10,數(shù)據(jù)庫為20。
終上所述,第一種失敗的情況是比第二種失敗的情況多的,因?yàn)榈谝环N情況出現(xiàn)錯誤的時間是在刪除緩存并更新數(shù)據(jù)庫后,線程2有著充足的時間在這段時間內(nèi)寫入緩存,而對于第二種情況來說,出現(xiàn)問題發(fā)生在查完數(shù)據(jù)庫到寫入緩存這段時間內(nèi),這段時間幾乎是毫秒級別的,線程2在這段時間內(nèi)更新數(shù)據(jù)庫并刪除緩存,顯然幾率是很低的(除了高高高并發(fā)的狀況下),所以我們選擇先操作數(shù)據(jù)庫在操作緩存~
總結(jié):
緩存更新策略的最佳實(shí)踐方案:
1.低一致性需求:使用Redis自帶的內(nèi)存淘汰機(jī)制
2.高一致性需求:主動更新,并以超時剔除作為兜底方案
主動更新時
進(jìn)行讀操作:
- 緩存命中則直接返回
- 緩存未命中則查詢數(shù)據(jù)庫,并寫入緩存,設(shè)定超時時間
進(jìn)行寫操作:
- 先寫數(shù)據(jù)庫,然后再刪除緩存
- 要確保數(shù)據(jù)庫與緩存操作的原子性
3.3 主動更新策略練習(xí)
修改商品緩存
1. 根據(jù)id查詢店鋪時,如果緩存未命中,則查詢數(shù)據(jù)庫,將數(shù)據(jù)庫結(jié)果寫入緩存,并設(shè)置超時時間
2. 根據(jù)id修改店鋪時,先修改數(shù)據(jù)庫,再刪除緩存
實(shí)現(xiàn)代碼如下(Service層實(shí)現(xiàn)):
package com.hmdp.service.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY; import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryShopById(Long id) { //1.去redis中查詢商品是否存在 String key = CACHE_SHOP_KEY+id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ //2.存在,直接返回給用戶 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //3.不存在,帶著id去數(shù)據(jù)庫查詢是否存在商品 Shop shop = getById(id); if (shop == null){ //4.不存在,返回錯誤信息 Result.fail("商品信息不存在!"); } //5.存在,存入redis(并設(shè)置超時時間) stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); //6.返回商品信息 return Result.ok(shop); } @Override @Transactional public Result updateShop(Shop shop) { //1.先更新數(shù)據(jù)庫 updateById(shop); //2.刪除redis緩存 String key = CACHE_SHOP_KEY+shop.getId(); stringRedisTemplate.delete(key); return null; } }
這樣就能達(dá)到基本的主動緩存更新啦~
4 緩存穿透及其解決方案
4.1 緩存穿透的概念
緩存穿透是指客戶端請求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,這樣緩存永遠(yuǎn)不會生效,這些請求都會打到數(shù)據(jù)庫。
通俗的說,就是請求的數(shù)據(jù)在數(shù)據(jù)庫和緩存中都沒有,最后請求都到了數(shù)據(jù)庫。這個時候,如果有一個惡意的程序員,通過某種方式不斷請求一個不存在的數(shù)據(jù),這樣就會給數(shù)據(jù)庫帶來巨大的壓力。(就怕用戶懂代碼)
4.2 解決方案及實(shí)現(xiàn)
常見的緩存穿透的解決方案有兩種:
1.就是給redis緩存一個空對象并設(shè)置TTL存活時間
這種方式優(yōu)點(diǎn)在于實(shí)現(xiàn)簡單,維護(hù)方便,但也帶來了額外的內(nèi)存消耗和可能的短期的數(shù)據(jù)不一致。
小知識:這里的數(shù)據(jù)不一致發(fā)生在用戶剛從redis中拿到null值恰好數(shù)據(jù)插入了這個請求需要的值而導(dǎo)致的數(shù)據(jù)庫Redis數(shù)據(jù)不一致。
2. 就是利用布隆過濾
通俗的說,就是中間件~
這種方式 優(yōu)點(diǎn)在于內(nèi)存消耗較小,沒有多余的key,缺點(diǎn)就在于實(shí)現(xiàn)復(fù)雜,而且布隆過濾器有誤判的可能...
代碼實(shí)現(xiàn):這里實(shí)現(xiàn)第一種
就拿上述的寫入商品信息為例:
我們只需要在數(shù)據(jù)庫獲取數(shù)據(jù)時,如果取到空值,不直接返回404,而是將空值也存入redis中,并且在判斷緩存是否命中時,判斷命中的值是不是我們傳入的空值。如果是,直接結(jié)束,不是就返回商鋪信息
package com.hmdp.service.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryShopById(Long id) { //1.去redis中查詢商品是否存在 String key = CACHE_SHOP_KEY+id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ //2.存在,直接返回給用戶 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //判斷命中的是否為空字符串 /** * (這里重點(diǎn)講一下為什么是不等于:因?yàn)槲覀儷@取到的shopJson為null是,上面的isNotBlank方法會返回false,導(dǎo)致成為了不命中的效果) */ if (shopJson != null){ return Result.fail("商品信息不存在!"); } //3.不存在,帶著id去數(shù)據(jù)庫查詢是否存在商品 Shop shop = getById(id); if (shop == null){ // 4.將空值存入redis stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES); //5.不存在,返回錯誤信息 Result.fail("商品信息不存在!"); } //6.存在,存入redis(并設(shè)置超時時間) stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); //7.返回商品信息 return Result.ok(shop); } }
總結(jié):
緩存穿透產(chǎn)生的原因是什么?
用戶請求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,不斷發(fā)起這樣的請求,給數(shù)據(jù)庫帶來巨大壓力。
緩存穿透的解決方案有哪些?(3-6點(diǎn)為擴(kuò)展~)
1.緩存null值
2.布隆過濾
3.增強(qiáng)id的復(fù)雜度,避免被猜測id規(guī)律
4.做好數(shù)據(jù)的基礎(chǔ)格式校驗(yàn)
5.加強(qiáng)用戶權(quán)限校驗(yàn)
6.做好熱點(diǎn)參數(shù)的限流
5 緩存雪崩的概念及其解決方案
緩存雪崩是指在同一時段大量的緩存key同時失效或者Redis服務(wù)宕機(jī),導(dǎo)致大量請求到達(dá)數(shù)據(jù)庫,帶來巨大壓力。
就是說,一群設(shè)置了有效期的key同時消失了,或者說redis罷工了,導(dǎo)致所有的或者說大量的請求會給數(shù)據(jù)庫帶來巨大壓力叫做緩存雪崩~
解決方式也比較的簡單~
1. 給不同的key添加隨機(jī)的TTL存活時間(這種就是最簡單的,設(shè)置存貨時間隨機(jī)各不相同)
2. 利用Redis集群(這種針對與Redis出現(xiàn)宕機(jī)的情況)
3. 給緩存業(yè)務(wù)添加降級限流策略(SpringCloud知識點(diǎn))
4. 給業(yè)務(wù)添加多級緩存
6 緩存擊穿及解決方案
6.1 什么是緩存擊穿
緩存擊穿問題也叫熱點(diǎn)Key問題,就是一個被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無數(shù)的請求訪問會在瞬間給數(shù)據(jù)庫帶來巨大的沖擊。
大概的奔潰流程是這樣子的:
第一個線程,查詢r(jià)edis發(fā)現(xiàn)未命中,然后去數(shù)據(jù)庫查詢并重建緩存,這個時候因?yàn)樵诰彺嬷亟I(yè)務(wù)較為復(fù)雜的情況下,重建時間較久,又因?yàn)楦卟l(fā)的環(huán)境下,在線程1重建緩存的時間內(nèi),會有其他的大量的其他線程進(jìn)來,發(fā)現(xiàn)查找緩存仍未命中,導(dǎo)致繼續(xù)重建,如此死循環(huán)。
6.2 緩存擊穿解決方法
常見的解決方案有兩種:
6.2.1 互斥鎖
互斥鎖的解決方案如下:
就是當(dāng)線程查詢緩存未命中時,嘗試去獲取互斥鎖,然后在重建緩存數(shù)據(jù),在這段時間里,其他線程也會去嘗試獲取互斥鎖,如果失敗就休眠一段時間,并繼續(xù),不斷重試,等到數(shù)據(jù)重建成功,其他線程就可以命中數(shù)據(jù)了。這樣就不會導(dǎo)致緩存擊穿。這個方案數(shù)據(jù)一致性是絕對的,但是相對來說會犧牲性能。
實(shí)現(xiàn)方法:
1.獲取互斥鎖和釋放鎖的實(shí)現(xiàn):
這里我們獲取互斥鎖可以使用redis中string類型中的setnx方法 ,因?yàn)閟etnx方法是在key不存在的情況下才可以創(chuàng)建成功的,所以我們重建緩存時,使用setnx來將鎖的數(shù)據(jù)加入到redis中,并且通過判斷這個鎖的key是否存在,如果存在就是獲取鎖成功,失敗就是獲取失敗,這樣剛好可以實(shí)現(xiàn)互斥鎖的效果。
而釋放鎖就更簡單了,直接刪除我們存入的鎖的key來釋放鎖。
//獲取鎖 public Boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } //釋放鎖方法 public void unlock(String key){ stringRedisTemplate.delete(key); }
2.實(shí)現(xiàn)代碼:
具體操作流程如下圖:
實(shí)現(xiàn)代碼如下:
package com.hmdp.service.impl; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisData; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryShopById(Long id) { //互斥鎖緩存擊穿 Shop shop = queryWithMutex(id); if (shop == null){ Result.fail("商品信息不存在!"); } //8.返回商品信息 return Result.ok(shop); } // 緩存穿透解決——互斥鎖 public Shop queryWithMutex(Long id){ //1.去redis中查詢商品是否存在 String key = CACHE_SHOP_KEY+id; String shopJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)){ //2.存在,直接返回給用戶 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } //判斷命中的是否為空字符串 if (shopJson != null){ return null; } Shop shop = null; String lockKey = LOCK_SHOP_KEY+id; try { //3.緩存重建 //3.1嘗試獲取鎖 Boolean isLock = tryLock(lockKey); //3.2判斷獲取鎖是否成功,如果休眠重試 if (!isLock){ //休眠 Thread.sleep(50); //重試用遞歸 queryWithMutex(id); } //3.3如果成功,去數(shù)據(jù)庫查找數(shù)據(jù) shop = getById(id); if (shop == null){ // 4.將空值存入redis stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES); //5.不存在,返回錯誤信息 Result.fail("商品信息不存在!"); } //6.存在,存入redis(并設(shè)置超時時間) stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //7.釋放鎖 unlock(lockKey); } //8.返回商品信息 return shop; } //獲取鎖 public Boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } //釋放鎖方法 public void unlock(String key){ stringRedisTemplate.delete(key); } }
6.2.2 邏輯過期
邏輯過期的處理方法主要為:
給redis緩存字段中添加一個過期時間,然后當(dāng)線程查詢數(shù)據(jù)庫的時候,先判斷是否已經(jīng)過期,如果過期,就獲取獲取互斥鎖,并開啟一個子線程進(jìn)行緩存重建任務(wù),直到子線程完成任務(wù)后,釋放鎖。在這段時間內(nèi),其他線程獲取互斥鎖失敗后,并不是繼續(xù)等待重試,而是直接返回舊數(shù)據(jù)。這個方法雖然性能較好,但也犧牲了數(shù)據(jù)一致性。
實(shí)現(xiàn)方法:
1.獲取互斥鎖和釋放鎖如上
2.給redis數(shù)據(jù)添加一個過期時間(創(chuàng)建一個RedisData類,并封裝數(shù)據(jù))
RedisData類:
package com.hmdp.utils; import lombok.Data; import java.time.LocalDateTime; @Data public class RedisData { private LocalDateTime expireTime; private Object data; }
//給商品信息添加一個過期時間字段,并存入redis當(dāng)中 public void saveShop2Redis(Long id,Long expireSeconds){ Shop shop = getById(id); RedisData redisData = new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData)); }
然后就是業(yè)務(wù)實(shí)現(xiàn)了:
具體代碼如下:
package com.hmdp.service.impl; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisData; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.*; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryShopById(Long id) { //邏輯過期解決緩存擊穿問題 // Shop shop = queryWithLogicalExpire(id); if (shop == null){ Result.fail("商品信息不存在!"); } //8.返回商品信息 return Result.ok(shop); } private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public Shop queryWithLogicalExpire(Long id){ //1.去redis中查詢商品是否存在 String key = CACHE_SHOP_KEY+id; String shopJson = stringRedisTemplate.opsForValue().get(key); //2.判斷是否命中 //3.未命中 if (StrUtil.isBlank(shopJson)){ return null; } //4.命中,先把redis中的數(shù)據(jù)反序列化成java對象 RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); //4.1獲取過期時間 LocalDateTime expireTime = redisData.getExpireTime(); //4.2獲取商品對象 JSONObject data = (JSONObject) redisData.getData(); Shop shop = JSONUtil.toBean(data, Shop.class); //5.判斷是否過期 if (expireTime.isAfter(LocalDateTime.now())){ //未過期,直接返回shop return shop; } //6.過期,重建緩存 //6.1嘗試獲取鎖,并判斷 String lockKey = LOCK_SHOP_KEY + id; Boolean isLock = tryLock(lockKey); if (isLock){ //5.2 如果成功,開啟一個獨(dú)立的線程,重建緩存 CACHE_REBUILD_EXECUTOR.submit(()->{ try { //重建緩存 this.saveShop2Redis(id,20L); } catch (Exception e) { throw new RuntimeException(e); } finally { //釋放鎖 unlock(lockKey); } }); } //6.2返回舊的商品信息 return shop; } //獲取鎖 public Boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } //釋放鎖方法 public void unlock(String key){ stringRedisTemplate.delete(key); } //給商品信息添加一個過期時間字段,并存入redis當(dāng)中 public void saveShop2Redis(Long id,Long expireSeconds){ Shop shop = getById(id); RedisData redisData = new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData)); } }
緩存知識結(jié)束。
到此這篇關(guān)于Redis緩存實(shí)例超詳細(xì)講解的文章就介紹到這了,更多相關(guān)Redis緩存策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis數(shù)據(jù)結(jié)構(gòu)之跳躍表使用學(xué)習(xí)
這篇文章主要為大家介紹了Redis數(shù)據(jù)結(jié)構(gòu)之跳躍表使用學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07redis中scan命令的基本實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于redis中scan命令的基本實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10如何利用Redis?List實(shí)現(xiàn)Java數(shù)據(jù)庫分頁快速查詢
這篇文章主要給大家介紹了關(guān)于如何利用Redis?List實(shí)現(xiàn)Java數(shù)據(jù)庫分頁快速查詢的相關(guān)資料,Redis是一個高效的內(nèi)存數(shù)據(jù)庫,它支持包括String、List、Set、SortedSet和Hash等數(shù)據(jù)類型的存儲,需要的朋友可以參考下2024-02-02