解决redis 缓存穿透 缓存击穿 小案例

普通查询
  • 存在缓存穿透风险
  • 存在缓存击穿风险
  1. controller
java 复制代码
package com.orchids.redisapply.controller;


import com.orchids.redisapply.domain.po.Shop;
import com.orchids.redisapply.service.IShopService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author nullpointer
 * @since 2024-08-01
 */
@Api(tags = "商店信息")
@RestController
@RequestMapping("/shop")
@RequiredArgsConstructor
public class ShopController {
    private final IShopService shopService;
    @ApiOperation("根据id查询商店信息")
    @GetMapping("/{id}")
    public Shop queryShopById(@PathVariable("id") Long id){
        return shopService.queryShopById(id);
    }

}
  1. service
java 复制代码
package com.orchids.redisapply.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.orchids.redisapply.domain.po.Shop;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author nullpointer
 * @since 2024-08-01
 */
public interface IShopService extends IService<Shop> {

    Shop queryShopById(Long id);
    
}
  1. serviceImpl
java 复制代码
package com.orchids.redisapply.service.impl;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.orchids.redisapply.domain.po.Shop;
import com.orchids.redisapply.mapper.ShopMapper;
import com.orchids.redisapply.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import static com.orchids.redisapply.domain.constant.RedisConstants.CACHE_SHOP_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author nullpointer
 * @since 2024-08-01
 */
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    private final ShopMapper shopMapper;
    private final StringRedisTemplate redisTemplate;
    @Override
    //根据商铺id查询商铺信息
    public Shop queryShopById(Long id) {
        //1.判断redis中是否有缓存
        String key = CACHE_SHOP_KEY+id;
        String shopInfo = redisTemplate.opsForValue().get(key);
        //2.缓存中存在
        if (StrUtil.isNotBlank(shopInfo)) {
            return JSONUtil.toBean(shopInfo, Shop.class);
        }
        //3.没有缓存查数据库
        Shop shop = getById(id);
        if (shop == null){
            throw new RuntimeException("该商铺不存在");
        }
        //4.重建缓存
        String str = JSONUtil.toJsonStr(shop);
        redisTemplate.opsForValue().set(key,str);
        return shop;
    }
}
  1. 测试
  • 第一次查询数据库
  • 之后都查redis
  • 响应结果
缓存穿透
  1. 前端发送请求请求查询数据库中一定不存在的值 解决方式有
  • 填充空值
  • 布隆过滤器
  1. 填充null实现 如下
java 复制代码
    @Override  //填充空值解决缓存穿透
    public Shop queryShopById(Long id) {
        //1.判断redis中是否有缓存
        String key = CACHE_SHOP_KEY+id;
        String shopInfo = redisTemplate.opsForValue().get(key);
        //2.缓存中存在
        if (StrUtil.isNotBlank(shopInfo)) {
            //2.1判断是否为null
            if ("null".equals(shopInfo)){
                throw new RuntimeException("该商铺不存在");
            }
            return JSONUtil.toBean(shopInfo, Shop.class);
        }
        //3.没有缓存查数据库
        Shop shop = getById(id);
        if (shop == null){
            //3.1 redis中填充null值
            redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
            throw new RuntimeException("该商铺不存在");
        }
        //4.重建缓存
        String str = JSONUtil.toJsonStr(shop);
        redisTemplate.opsForValue().set(key,str);
        return shop;
    }

测试结果

发送不存在的请求 可以添加其他的返回值也可以直接抛出

缓存击穿
  1. 当热点key失效大量请求请求到数据库 可能压垮数据库解决方案有
  • 添加互斥锁
  • 逻辑过期
  1. 互斥锁解决
java 复制代码
    @Override
    public Shop queryShopById(Long id) {
        //1.判断redis中是否有缓存
        String key = CACHE_SHOP_KEY+id;
        String shopInfo = redisTemplate.opsForValue().get(key);
        //2.缓存中存在
        if (StrUtil.isNotBlank(shopInfo)) {
            //2.1判断是否为null
            if ("null".equals(shopInfo)){
                throw new RuntimeException("该商铺不存在");
            }
            return JSONUtil.toBean(shopInfo, Shop.class);
        }
        Shop shop = null;

        try {
            //3.0获取互斥
            boolean lock = tryLock(LOCK_SHOP_KEY+id);
            //3.1没有有获取到锁
            if (!lock){
                //睡个500毫秒再重试
                Thread.sleep(500);
                return queryShopById(id);
            }
            //3.2获取到了锁 查数据库重建缓存
            shop = getById(id);
            //3.3数据库中也没有就有恶意攻击风险
            if (shop == null){
                //3.4redis中填充null值
                redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
                throw new RuntimeException("该商铺不存在");
            }
            String str = JSONUtil.toJsonStr(shop);
            //缓存重建完成
            redisTemplate.opsForValue().set(key,str);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(LOCK_SHOP_KEY+id);
        }
        //释放锁
        return shop;
    }
    private boolean tryLock(String key){
        //锁也要添加过期时间 避免死锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(lock);
    }
    private void unLock(String key){
        redisTemplate.delete(key);
    }
  • 压测结果 只有一个线程去查询 数据库
  1. 逻辑过期解决
java 复制代码
 //逻辑过期解决缓存击穿
    public Shop queryShopByIdLogic(Long id) {
        //1.判断redis中是否有缓存
        String key = CACHE_SHOP_KEY+id;
        String redisData = redisTemplate.opsForValue().get(key);
        //2.缓存不存在
        if (StrUtil.isBlank(redisData)){
            throw new RuntimeException("该商铺不存在");
        }
        //3.缓存存在判断是否过期
        RedisData data = JSONUtil.toBean(redisData, RedisData.class);
        LocalDateTime expireTime = data.getExpireTime();
        Shop shop = (Shop) data.getData();
        //判断过期时间是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            return shop;
        }
        //过期缓存重建 使用线程池
        Shop reShop;
        try {
            //3.0获取互斥
            boolean lock = tryLock(LOCK_SHOP_KEY+id);
            //3.1没有有获取到锁
            if (!lock){
                //睡个500毫秒再重试
                Thread.sleep(50);
                return queryShopById(id);
            }
            //3.2获取到了锁 查数据库重建缓存
            reShop = getById(id);
            //模拟重建缓存要花很长时间
            Thread.sleep(100);
            //3.3数据库中也没有就有恶意攻击风险
            if (shop == null){
                //3.4redis中填充null值
                redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
                throw new RuntimeException("该商铺不存在");
            }
            String str = JSONUtil.toJsonStr(shop);
            //缓存重建完成
            redisTemplate.opsForValue().set(key,str);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(LOCK_SHOP_KEY+id);
        }
        //释放锁
        return reShop;
    }

    //缓存预热
    public void SaveRedis(Long id,Long expireTime){
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        redisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }
java 复制代码
package com.orchids.redisapply.domain.cache;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * @ Author qwh
 * @ Date 2024/8/1 9:08
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
相关推荐
程序员清风11 分钟前
缓存、索引与数据结构的工程选择
数据结构·spring·缓存
fox063322 分钟前
预约类平台的责任边界建模:从一个证件办理预约平台的流程设计说起
大数据·数据库·搜索引擎·全文检索·生活
John_Hui23 分钟前
MySQL基础知识
数据库·sql·mysql·1024程序员节
DolphinDB28 分钟前
QuantLib 之外,衍生品定价还能怎么选?DolphinDB Monte Carlo + PDE 工具实测
数据库·时序数据库·量化金融·dolphindb
泡海椒32 分钟前
深度解析JQuick-SQL 逻辑计划到物理计划生成机制|分布式SQL原理
数据库·分布式·sql
恒知学术34 分钟前
时间序列异常检测论文怎么查?我把 3 轮检索和筛选表写成了可复现流程
数据库·文献检索·谷歌学术
小豆庄1 小时前
视频脱敏:云端识别与本地推理的边界实测
数据库·yolo·自然语言处理·视觉检测·视频·设计规范·纹理贴图
wshzd1 小时前
LLM之Agent(120)|DeepSeek-Harness(二十七)Workflow 引擎与 Ralph 循环
前端·数据库·人工智能
code2cat1 小时前
【随笔】RAG语义缓存:问题很相似,答案就能直接复用吗
java·缓存·知识库·rag
l1t1 小时前
DeepSeek总结的DuckDB 2.0.0 中的隐藏亮点
大数据·数据库·duckdb