解决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;
}
相关推荐
老邓计算机毕设6 小时前
SSM学生选课系统xvbna(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学生选课系统·ssm 框架·高校教学管理
難釋懷7 小时前
SpringDataRedis数据序列化器
redis·缓存
枷锁—sha7 小时前
【PortSwigger Academy】SQL 注入绕过登录 (Login Bypass)
数据库·sql·学习·安全·网络安全
逍遥德9 小时前
PostgreSQL 中唯一约束(UNIQUE CONSTRAINT) 和唯一索引(UNIQUE INDEX) 的核心区别
数据库·sql·postgresql·dba
工业甲酰苯胺9 小时前
字符串分割并展开成表格的SQL实现方法
数据库·sql
科技块儿9 小时前
IP定位技术:游戏反外挂体系中的精准识别引擎
数据库·tcp/ip·游戏
衫水10 小时前
[特殊字符] MySQL 常用指令大全
数据库·mysql·oracle
卓怡学长10 小时前
m115乐购游戏商城系统
java·前端·数据库·spring boot·spring·游戏
小句10 小时前
SQL中JOIN语法详解 GROUP BY语法详解
数据库·sql
阿杰 AJie11 小时前
MySQL 里给表添加索引
数据库·mysql