解决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;
}
相关推荐
zzb15801 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
一只鹿鹿鹿1 小时前
信息安全等级保护安全建设防护解决方案(总体资料)
运维·开发语言·数据库·面试·职场和发展
堕2741 小时前
MySQL数据库《基础篇--数据库索引(2)》
数据库·mysql
wei_shuo1 小时前
数据库优化器进化论:金仓如何用智能下推把查询时间从秒级打到毫秒级
数据库·kingbase·金仓
雷工笔记1 小时前
Navicat Premium 17 软件安装记录
数据库
wenlonglanying2 小时前
Ubuntu 系统下安装 Nginx
数据库·nginx·ubuntu
数据库小组2 小时前
10 分钟搞定!Docker 一键部署 NineData 社区版
数据库·docker·容器·database·数据库管理工具·ninedata·迁移工具
爬山算法2 小时前
MongoDB(38)如何使用聚合进行投影?
数据库·mongodb
l1t2 小时前
Deep Seek总结的APSW 和 SQLite 的关系
数据库·sqlite
Pocker_Spades_A3 小时前
基于代价模型的连接条件下推:复杂SQL查询的性能优化实践
数据库·sql·性能优化