Redis緩存實例超詳細(xì)講解
1 前言
1.1 什么是緩存
緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache [ kæ? ] ),是存貯數(shù)據(jù)的臨時地方,一般讀寫性能較高。
緩存有很多中實現(xiàn)場景:對于web開發(fā),常見的有如下幾種:

而我們的Redis緩存功能就是屬于在應(yīng)用層緩存 。
1.2 緩存的作用及成本
作用:毫無疑問,就是提高讀寫的效率,有效降低后端服務(wù)器的負(fù)載,有效降低響應(yīng)時間。
成本:任何東西都有兩面性,緩存在帶來高效的讀寫效率的同時,也有著對應(yīng)的從成本。
比如:數(shù)據(jù)一致性成本、代碼維護(hù)成本、運維成本等。

1.3 Redis緩存模型
如下圖

原本的模型應(yīng)該是客戶端發(fā)送請求給數(shù)據(jù)庫,數(shù)據(jù)庫返回數(shù)據(jù)給客戶端,而Reids的緩存模型就是在原有的基礎(chǔ)上,在中間加上一層Redis(經(jīng)典的中間件思想~)用戶每次都會先去redis中查找數(shù)據(jù),如果未命中才會去數(shù)據(jù)庫中查找數(shù)據(jù),并寫入Reis當(dāng)中,這么一來,用于下次需要相同的數(shù)據(jù)的時候,就可以在Reis當(dāng)中進(jìn)行獲取,又因為Redis的高讀寫效率,實現(xiàn)了緩存的效果~
2 給商戶信息添加緩存
基于上述的Redis緩存模型,我們可以得出下面的緩存添加邏輯:

代碼實現(xiàn):(直接看Service層實現(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ù)實現(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)存淘汰機制,當(dāng)內(nèi)存不足的時候自動淘汰,下次請求時再繼續(xù)存入數(shù)據(jù)。
這種策略模型優(yōu)點在于沒有維護(hù)成本,但是內(nèi)存不足這種無法預(yù)定的情況就導(dǎo)致了緩存中會有很多舊的數(shù)據(jù),數(shù)據(jù)一致性差。
2. 超時剔除:這種策略就是比較實用的,就是給緩存添加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)存淘汰機制。例如店鋪類型的查詢緩存。
當(dāng)有高一致性需求:使用主動更新,并以超時剔除作為兜底方案。例如店鋪詳情查詢的緩存
3.2 主動更新策略
上述提到的主動更新策略,無疑是維護(hù)成本最高的,但具體又有哪些維護(hù)方式呢?怎么去做主動更新維護(hù)呢?
如下圖:主動更新主要分為下面三種:

又因為后兩種實現(xiàn)方式過于復(fù)雜,所以重點說第一種。
第一種:Cache Aside Pattern,由緩存調(diào)用者進(jìn)行操作,就是在我們數(shù)據(jù)庫進(jìn)行更新時,對緩存也進(jìn)行更新。
這又引出了好幾個問題了~
1. 緩存更新?是更新緩存還是直接刪除緩存?
2. 如何保證數(shù)據(jù)庫更新和緩存更新同時成功或失???
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)問題的流程(右邊)
補充一下:這邊兩個線程最初的數(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。
終上所述,第一種失敗的情況是比第二種失敗的情況多的,因為第一種情況出現(xiàn)錯誤的時間是在刪除緩存并更新數(shù)據(jù)庫后,線程2有著充足的時間在這段時間內(nèi)寫入緩存,而對于第二種情況來說,出現(xiàn)問題發(fā)生在查完數(shù)據(jù)庫到寫入緩存這段時間內(nèi),這段時間幾乎是毫秒級別的,線程2在這段時間內(nèi)更新數(shù)據(jù)庫并刪除緩存,顯然幾率是很低的(除了高高高并發(fā)的狀況下),所以我們選擇先操作數(shù)據(jù)庫在操作緩存~
總結(jié):
緩存更新策略的最佳實踐方案:
1.低一致性需求:使用Redis自帶的內(nèi)存淘汰機制
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ù)庫,再刪除緩存
實現(xiàn)代碼如下(Service層實現(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ù)實現(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 解決方案及實現(xiàn)
常見的緩存穿透的解決方案有兩種:
1.就是給redis緩存一個空對象并設(shè)置TTL存活時間
這種方式優(yōu)點在于實現(xiàn)簡單,維護(hù)方便,但也帶來了額外的內(nèi)存消耗和可能的短期的數(shù)據(jù)不一致。
小知識:這里的數(shù)據(jù)不一致發(fā)生在用戶剛從redis中拿到null值恰好數(shù)據(jù)插入了這個請求需要的值而導(dǎo)致的數(shù)據(jù)庫Redis數(shù)據(jù)不一致。

2. 就是利用布隆過濾
通俗的說,就是中間件~
這種方式 優(yōu)點在于內(nèi)存消耗較小,沒有多余的key,缺點就在于實現(xiàn)復(fù)雜,而且布隆過濾器有誤判的可能...

代碼實現(xiàn):這里實現(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ù)實現(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);
}
//判斷命中的是否為空字符串
/**
* (這里重點講一下為什么是不等于:因為我們獲取到的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點為擴展~)
1.緩存null值
2.布隆過濾
3.增強id的復(fù)雜度,避免被猜測id規(guī)律
4.做好數(shù)據(jù)的基礎(chǔ)格式校驗
5.加強用戶權(quán)限校驗
6.做好熱點參數(shù)的限流
5 緩存雪崩的概念及其解決方案
緩存雪崩是指在同一時段大量的緩存key同時失效或者Redis服務(wù)宕機,導(dǎo)致大量請求到達(dá)數(shù)據(jù)庫,帶來巨大壓力。
就是說,一群設(shè)置了有效期的key同時消失了,或者說redis罷工了,導(dǎo)致所有的或者說大量的請求會給數(shù)據(jù)庫帶來巨大壓力叫做緩存雪崩~

解決方式也比較的簡單~
1. 給不同的key添加隨機的TTL存活時間(這種就是最簡單的,設(shè)置存貨時間隨機各不相同)
2. 利用Redis集群(這種針對與Redis出現(xiàn)宕機的情況)
3. 給緩存業(yè)務(wù)添加降級限流策略(SpringCloud知識點)
4. 給業(yè)務(wù)添加多級緩存
6 緩存擊穿及解決方案
6.1 什么是緩存擊穿
緩存擊穿問題也叫熱點Key問題,就是一個被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無數(shù)的請求訪問會在瞬間給數(shù)據(jù)庫帶來巨大的沖擊。
大概的奔潰流程是這樣子的:
第一個線程,查詢redis發(fā)現(xiàn)未命中,然后去數(shù)據(jù)庫查詢并重建緩存,這個時候因為在緩存重建業(yè)務(wù)較為復(fù)雜的情況下,重建時間較久,又因為高并發(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ù)一致性是絕對的,但是相對來說會犧牲性能。

實現(xiàn)方法:
1.獲取互斥鎖和釋放鎖的實現(xiàn):
這里我們獲取互斥鎖可以使用redis中string類型中的setnx方法 ,因為setnx方法是在key不存在的情況下才可以創(chuàng)建成功的,所以我們重建緩存時,使用setnx來將鎖的數(shù)據(jù)加入到redis中,并且通過判斷這個鎖的key是否存在,如果存在就是獲取鎖成功,失敗就是獲取失敗,這樣剛好可以實現(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.實現(xiàn)代碼:
具體操作流程如下圖:

實現(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ù)實現(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ù)一致性。

實現(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ù)實現(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ù)實現(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 如果成功,開啟一個獨立的線程,重建緩存
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緩存實例超詳細(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-07
如何利用Redis?List實現(xiàn)Java數(shù)據(jù)庫分頁快速查詢
這篇文章主要給大家介紹了關(guān)于如何利用Redis?List實現(xiàn)Java數(shù)據(jù)庫分頁快速查詢的相關(guān)資料,Redis是一個高效的內(nèi)存數(shù)據(jù)庫,它支持包括String、List、Set、SortedSet和Hash等數(shù)據(jù)類型的存儲,需要的朋友可以參考下2024-02-02

