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

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

1.3 Redis緩存模型
如下圖

原本的模型應(yīng)該是客戶端發(fā)送請(qǐng)求給數(shù)據(jù)庫,數(shù)據(jù)庫返回?cái)?shù)據(jù)給客戶端,而Reids的緩存模型就是在原有的基礎(chǔ)上,在中間加上一層Redis(經(jīng)典的中間件思想~)用戶每次都會(huì)先去redis中查找數(shù)據(jù),如果未命中才會(huì)去數(shù)據(jù)庫中查找數(shù)據(jù),并寫入Reis當(dāng)中,這么一來,用于下次需要相同的數(shù)據(jù)的時(shí)候,就可以在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.不存在,返回錯(cuò)誤信息
Result.fail("商品信息不存在!");
}
//5.存在,存入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//6.返回商品信息
return Result.ok(shop);
}
}3 緩存更新策略
3.1 更新策略介紹
使用Redis做緩存時(shí),緩存還是要及時(shí)更新的,不然就會(huì)出現(xiàn)數(shù)據(jù)庫和緩存數(shù)據(jù)不一致的情況,也是我們常說的Redis成本——數(shù)據(jù)一致性成本
緩存大致有以下三種更新策略:
1. 內(nèi)存淘汰策略:這種策略沒有維護(hù)成本,這是利用Redis的內(nèi)存淘汰機(jī)制,當(dāng)內(nèi)存不足的時(shí)候自動(dòng)淘汰,下次請(qǐng)求時(shí)再繼續(xù)存入數(shù)據(jù)。
這種策略模型優(yōu)點(diǎn)在于沒有維護(hù)成本,但是內(nèi)存不足這種無法預(yù)定的情況就導(dǎo)致了緩存中會(huì)有很多舊的數(shù)據(jù),數(shù)據(jù)一致性差。
2. 超時(shí)剔除:這種策略就是比較實(shí)用的,就是給緩存添加TTL存活時(shí)間,下次查詢是更新緩存。
這種策略數(shù)據(jù)一致性一般,維護(hù)成本有但是較低,一般用于兜底方案~
3.主動(dòng)更新策略:就是我們程序員手動(dòng)的進(jìn)行數(shù)據(jù)庫和緩存之間的更新,但數(shù)據(jù)庫更新時(shí),緩存也進(jìn)行更新。
這種策略數(shù)據(jù)一致性就是最高的(畢竟自己動(dòng)手,豐衣足食),但同時(shí)維護(hù)成本也是最高的。

那么,又該如何選擇緩存更新策略呢?
我的覺得應(yīng)該是根據(jù)業(yè)務(wù)場景不同來選擇不同的更新策略:
當(dāng)數(shù)據(jù)一致性要求低時(shí):l使用內(nèi)存淘汰機(jī)制。例如店鋪類型的查詢緩存。
當(dāng)有高一致性需求:使用主動(dòng)更新,并以超時(shí)剔除作為兜底方案。例如店鋪詳情查詢的緩存
3.2 主動(dòng)更新策略
上述提到的主動(dòng)更新策略,無疑是維護(hù)成本最高的,但具體又有哪些維護(hù)方式呢?怎么去做主動(dòng)更新維護(hù)呢?
如下圖:主動(dòng)更新主要分為下面三種:

又因?yàn)楹髢煞N實(shí)現(xiàn)方式過于復(fù)雜,所以重點(diǎn)說第一種。
第一種:Cache Aside Pattern,由緩存調(diào)用者進(jìn)行操作,就是在我們數(shù)據(jù)庫進(jìn)行更新時(shí),對(duì)緩存也進(jìn)行更新。
這又引出了好幾個(gè)問題了~
1. 緩存更新?是更新緩存還是直接刪除緩存?
2. 如何保證數(shù)據(jù)庫更新和緩存更新同時(shí)成功或失敗?
3. 操作時(shí)應(yīng)該先操作緩存還是先操作數(shù)據(jù)庫?
第一個(gè)問題:
我想說,緩存更新如果是數(shù)據(jù)更新的話,每次更新數(shù)據(jù)庫都要對(duì)緩存數(shù)據(jù)進(jìn)行更新,有太多無效的讀寫操作,所以操作緩存時(shí),選擇刪除緩存~
第二個(gè)問題:
要做到兩個(gè)操作一致性,第一想到的就應(yīng)該是事務(wù)。
解決方案:當(dāng)我們是單體系統(tǒng)時(shí),將緩存和數(shù)據(jù)庫操作放在同一個(gè)事務(wù)里。
當(dāng)我們是分布式系統(tǒng)時(shí),利用TTC等分布式事務(wù)方案
最后一個(gè)問題:先操作數(shù)據(jù)庫還是先操作緩存?
就只有下面兩種情況:
1.先刪除緩存,再操作數(shù)據(jù)庫
2.先操作數(shù)據(jù)庫,再刪除緩存
我們可以兩種情況都來看看~
第一種情況,先刪除緩存的情況,我們想的正常的不會(huì)出現(xiàn)問題的操作流程(左邊)和操作會(huì)出現(xiàn)問題的流程(右邊)
補(bǔ)充一下:這邊兩個(gè)線程最初的數(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ù)庫的情況
正確情況(左邊)錯(cuò)誤情況(右邊),數(shù)據(jù)庫和緩存最初數(shù)據(jù)也都還是10~

第二種先操作數(shù)據(jù)庫的方式,它出現(xiàn)的錯(cuò)誤情況主要是,線程1線程查詢了緩存,未命中后去查詢數(shù)據(jù)庫的同時(shí),線程2更新了數(shù)據(jù)庫,并且刪除了緩存,然后線程1才把數(shù)據(jù)庫中查出來的10寫入緩存,導(dǎo)致緩存為10,數(shù)據(jù)庫為20。
終上所述,第一種失敗的情況是比第二種失敗的情況多的,因?yàn)榈谝环N情況出現(xiàn)錯(cuò)誤的時(shí)間是在刪除緩存并更新數(shù)據(jù)庫后,線程2有著充足的時(shí)間在這段時(shí)間內(nèi)寫入緩存,而對(duì)于第二種情況來說,出現(xiàn)問題發(fā)生在查完數(shù)據(jù)庫到寫入緩存這段時(shí)間內(nèi),這段時(shí)間幾乎是毫秒級(jí)別的,線程2在這段時(shí)間內(nèi)更新數(shù)據(jù)庫并刪除緩存,顯然幾率是很低的(除了高高高并發(fā)的狀況下),所以我們選擇先操作數(shù)據(jù)庫在操作緩存~
總結(jié):
緩存更新策略的最佳實(shí)踐方案:
1.低一致性需求:使用Redis自帶的內(nèi)存淘汰機(jī)制
2.高一致性需求:主動(dòng)更新,并以超時(shí)剔除作為兜底方案
主動(dòng)更新時(shí)
進(jìn)行讀操作:
- 緩存命中則直接返回
- 緩存未命中則查詢數(shù)據(jù)庫,并寫入緩存,設(shè)定超時(shí)時(shí)間
進(jìn)行寫操作:
- 先寫數(shù)據(jù)庫,然后再刪除緩存
- 要確保數(shù)據(jù)庫與緩存操作的原子性
3.3 主動(dòng)更新策略練習(xí)
修改商品緩存
1. 根據(jù)id查詢店鋪時(shí),如果緩存未命中,則查詢數(shù)據(jù)庫,將數(shù)據(jù)庫結(jié)果寫入緩存,并設(shè)置超時(shí)時(shí)間
2. 根據(jù)id修改店鋪時(shí),先修改數(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.不存在,返回錯(cuò)誤信息
Result.fail("商品信息不存在!");
}
//5.存在,存入redis(并設(shè)置超時(shí)時(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á)到基本的主動(dòng)緩存更新啦~
4 緩存穿透及其解決方案
4.1 緩存穿透的概念
緩存穿透是指客戶端請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,這樣緩存永遠(yuǎn)不會(huì)生效,這些請(qǐng)求都會(huì)打到數(shù)據(jù)庫。
通俗的說,就是請(qǐng)求的數(shù)據(jù)在數(shù)據(jù)庫和緩存中都沒有,最后請(qǐng)求都到了數(shù)據(jù)庫。這個(gè)時(shí)候,如果有一個(gè)惡意的程序員,通過某種方式不斷請(qǐng)求一個(gè)不存在的數(shù)據(jù),這樣就會(huì)給數(shù)據(jù)庫帶來巨大的壓力。(就怕用戶懂代碼)
4.2 解決方案及實(shí)現(xiàn)
常見的緩存穿透的解決方案有兩種:
1.就是給redis緩存一個(gè)空對(duì)象并設(shè)置TTL存活時(shí)間
這種方式優(yōu)點(diǎn)在于實(shí)現(xiàn)簡單,維護(hù)方便,但也帶來了額外的內(nèi)存消耗和可能的短期的數(shù)據(jù)不一致。
小知識(shí):這里的數(shù)據(jù)不一致發(fā)生在用戶剛從redis中拿到null值恰好數(shù)據(jù)插入了這個(gè)請(qǐng)求需要的值而導(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ù)時(shí),如果取到空值,不直接返回404,而是將空值也存入redis中,并且在判斷緩存是否命中時(shí),判斷命中的值是不是我們傳入的空值。如果是,直接結(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方法會(huì)返回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.不存在,返回錯(cuò)誤信息
Result.fail("商品信息不存在!");
}
//6.存在,存入redis(并設(shè)置超時(shí)時(shí)間)
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回商品信息
return Result.ok(shop);
}
}總結(jié):
緩存穿透產(chǎn)生的原因是什么?
用戶請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,不斷發(fā)起這樣的請(qǐng)求,給數(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 緩存雪崩的概念及其解決方案
緩存雪崩是指在同一時(shí)段大量的緩存key同時(shí)失效或者Redis服務(wù)宕機(jī),導(dǎo)致大量請(qǐng)求到達(dá)數(shù)據(jù)庫,帶來巨大壓力。
就是說,一群設(shè)置了有效期的key同時(shí)消失了,或者說redis罷工了,導(dǎo)致所有的或者說大量的請(qǐng)求會(huì)給數(shù)據(jù)庫帶來巨大壓力叫做緩存雪崩~

解決方式也比較的簡單~
1. 給不同的key添加隨機(jī)的TTL存活時(shí)間(這種就是最簡單的,設(shè)置存貨時(shí)間隨機(jī)各不相同)
2. 利用Redis集群(這種針對(duì)與Redis出現(xiàn)宕機(jī)的情況)
3. 給緩存業(yè)務(wù)添加降級(jí)限流策略(SpringCloud知識(shí)點(diǎn))
4. 給業(yè)務(wù)添加多級(jí)緩存
6 緩存擊穿及解決方案
6.1 什么是緩存擊穿
緩存擊穿問題也叫熱點(diǎn)Key問題,就是一個(gè)被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無數(shù)的請(qǐng)求訪問會(huì)在瞬間給數(shù)據(jù)庫帶來巨大的沖擊。
大概的奔潰流程是這樣子的:
第一個(gè)線程,查詢r(jià)edis發(fā)現(xiàn)未命中,然后去數(shù)據(jù)庫查詢并重建緩存,這個(gè)時(shí)候因?yàn)樵诰彺嬷亟I(yè)務(wù)較為復(fù)雜的情況下,重建時(shí)間較久,又因?yàn)楦卟l(fā)的環(huán)境下,在線程1重建緩存的時(shí)間內(nèi),會(huì)有其他的大量的其他線程進(jìn)來,發(fā)現(xiàn)查找緩存仍未命中,導(dǎo)致繼續(xù)重建,如此死循環(huán)。

6.2 緩存擊穿解決方法
常見的解決方案有兩種:
6.2.1 互斥鎖
互斥鎖的解決方案如下:
就是當(dāng)線程查詢緩存未命中時(shí),嘗試去獲取互斥鎖,然后在重建緩存數(shù)據(jù),在這段時(shí)間里,其他線程也會(huì)去嘗試獲取互斥鎖,如果失敗就休眠一段時(shí)間,并繼續(xù),不斷重試,等到數(shù)據(jù)重建成功,其他線程就可以命中數(shù)據(jù)了。這樣就不會(huì)導(dǎo)致緩存擊穿。這個(gè)方案數(shù)據(jù)一致性是絕對(duì)的,但是相對(duì)來說會(huì)犧牲性能。

實(shí)現(xiàn)方法:
1.獲取互斥鎖和釋放鎖的實(shí)現(xiàn):
這里我們獲取互斥鎖可以使用redis中string類型中的setnx方法 ,因?yàn)閟etnx方法是在key不存在的情況下才可以創(chuàng)建成功的,所以我們重建緩存時(shí),使用setnx來將鎖的數(shù)據(jù)加入到redis中,并且通過判斷這個(gè)鎖的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.不存在,返回錯(cuò)誤信息
Result.fail("商品信息不存在!");
}
//6.存在,存入redis(并設(shè)置超時(shí)時(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緩存字段中添加一個(gè)過期時(shí)間,然后當(dāng)線程查詢數(shù)據(jù)庫的時(shí)候,先判斷是否已經(jīng)過期,如果過期,就獲取獲取互斥鎖,并開啟一個(gè)子線程進(jìn)行緩存重建任務(wù),直到子線程完成任務(wù)后,釋放鎖。在這段時(shí)間內(nèi),其他線程獲取互斥鎖失敗后,并不是繼續(xù)等待重試,而是直接返回舊數(shù)據(jù)。這個(gè)方法雖然性能較好,但也犧牲了數(shù)據(jù)一致性。

實(shí)現(xiàn)方法:
1.獲取互斥鎖和釋放鎖如上
2.給redis數(shù)據(jù)添加一個(gè)過期時(shí)間(創(chuàng)建一個(gè)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;
}//給商品信息添加一個(gè)過期時(shí)間字段,并存入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對(duì)象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
//4.1獲取過期時(shí)間
LocalDateTime expireTime = redisData.getExpireTime();
//4.2獲取商品對(duì)象
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 如果成功,開啟一個(gè)獨(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);
}
//給商品信息添加一個(gè)過期時(shí)間字段,并存入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));
}
}緩存知識(shí)結(jié)束。
到此這篇關(guān)于Redis緩存實(shí)例超詳細(xì)講解的文章就介紹到這了,更多相關(guān)Redis緩存策略內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(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
一文詳解如何停止/重啟/啟動(dòng)Redis服務(wù)
Redis是當(dāng)前比較熱門的NOSQL系統(tǒng)之一,它是一個(gè)key-value存儲(chǔ)系統(tǒng),這篇文章主要給大家介紹了關(guān)于如何停止/重啟/啟動(dòng)Redis服務(wù)的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03
redis中scan命令的基本實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于redis中scan命令的基本實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(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是一個(gè)高效的內(nèi)存數(shù)據(jù)庫,它支持包括String、List、Set、SortedSet和Hash等數(shù)據(jù)類型的存儲(chǔ),需要的朋友可以參考下2024-02-02

