解决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;
}
相关推荐
tatasix3 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
秋意钟7 分钟前
缓存雪崩、缓存穿透【Redis】
redis
南城花随雪。15 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了17 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度18 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
简 洁 冬冬20 分钟前
046 购物车
redis·购物车
天海华兮21 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
雯0609~31 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
soulteary1 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
gma9991 小时前
Etcd 框架
数据库·etcd