当前位置: 首页 > article >正文

Redis的缓存穿透、缓存雪崩、缓存击穿问题及有效解决方案

目录

一、缓存穿透

1.简介

2.解决方案

3.修改前的代码

4.修改过后的代码

二、缓存雪崩

1.简介

2.解决方案

三、缓存击穿

1.简介

2.解决方案

3.用代码来实现互斥锁来解决缓存击穿

4.用代码来实现逻辑过期解决缓存击穿

四、缓存穿透和缓存击穿的区别


一、缓存穿透

1.简介

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

也就是有请求一请求到后端就一定会打到数据库(redis和MySQL都没有该数据),打一次不要紧,但要是有无数个请求同时打过来,那数据库大概率会崩溃,所以预防缓存穿透问题还是十分重要的。

2.解决方案

目前常见的解决方案有返回缓存空对象和布隆过滤两种方法,区别如下:

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:

      • 额外的内存消耗

      • 可能造成短期的不一致

  • 布隆过滤

    • 优点:内存占用较少,没有多余key

    • 缺点:

      • 实现复杂

      • 存在误判可能

缓存空对象的解决思路如下:

当一个请求直接打到数据库的时候,后端直接在Redis中返回个空对象,之后多次相同的请求都会止步于Redis,通俗易懂的话来说,要打打Redis,别打MySQL。

布隆过滤的解决思路如下:

布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

假设布隆过滤器判断这个数据不存在,则直接返回

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

3.修改前的代码

我们采用返回空对象的方法来缓解缓存穿透问题,修改前的代码如下:

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 根据id查询店铺信息
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //1.从redis中查询商铺缓存信息
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3、存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4、不存在,根据id查询数据库
        Shop shop = getById(id);
        //5、判断数据库中是否有数据
        if (shop == null) {
            //6、没有,返回错误
            return Result.fail("店铺不存在");
        }
        //7、有数据,返回,并将数据写入redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,jsonStr);
        //8、返回结果
        return Result.ok(shop);
    }
}

这段代码的详细分析在以下文章可查看:给查询业务添加Redis缓存

4.修改过后的代码

@Override
    public Result queryById(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //1.从redis中查询商铺缓存信息
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3、存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否为空值------------------------------------------->修改后
        if(shopJson != null){
            return Result.fail("店铺不存在");
        }
        //4、不存在,根据id查询数据库
        Shop shop = getById(id);
        //5、判断数据库中是否有数据
        if (shop == null) {
            //6、没有,返回空字符串给redis------------------------------->修改后
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        //7、有数据,返回,并将数据写入redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key,jsonStr,30L, TimeUnit.MINUTES);
        //8、返回结果
        return Result.ok(shop);
    }

二、缓存雪崩

1.简介

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

2.解决方案

  • 给不同的Key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

三、缓存击穿

1.简介

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

2.解决方案

常见的解决方案有两种:

  • 互斥锁

  • 逻辑过期

互斥锁的解决思路就是在Redis进行缓存重建时,拿到一个互斥锁,其他请求拿不到这个锁就是乖乖等待锁的释放。

逻辑过期的解决思路如下:

在存入redis的value中增加一个字段,该字段为过期时间加上x分钟,通过计算就知道这个数据是否逻辑上过期,事实上没过期一直存在redis中。

在redis进行缓存重建的时候,会另开一个线程进行重建并拿到互斥锁,其他线程拿不到数据想要缓存重建时也拿不到锁,那就直接返回旧数据。

互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响

逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

3.用代码来实现互斥锁来解决缓存击穿

其中这个互斥锁跟我们平时用的锁不太一样,不是synchronized等那些锁,这个互斥锁是我们自己编写的锁,其核心是利用redis中的setnx指令来实现这个互斥锁。(共享+先到先到+不会被其他线程修改)

获取锁和释放锁的代码如下:

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

Service代码如下:

public Shop queryWithMutex(Long id)  {
        String key = CACHE_SHOP_KEY + id;
        // 1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("key");
        // 2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的值是否是空值
        if (shopJson != null) {
            //返回一个错误信息
            return null;
        }
        // 4.实现缓存重构
        //4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断否获取成功
            if(!isLock){
                //4.3 失败,则休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4 成功,根据id查询数据库
             shop = getById(id);
            // 5.不存在,返回错误
            if(shop == null){
                 //将空值写入redis
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }
            //6.写入redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);

        }catch (Exception e){
            throw new RuntimeException(e);
        }
        finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
        return shop;
    }

4.用代码来实现逻辑过期解决缓存击穿

流程图如下:

具体代码如下:

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
    String key = CACHE_SHOP_KEY + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return shop;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        CACHE_REBUILD_EXECUTOR.submit( ()->{

            try{
                //重建缓存
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return shop;
}

四、缓存穿透和缓存击穿的区别

缓存穿透和缓存击穿之间的区别,可以说是既有质的区别也有量的区别:

  1. 质的区别

    • 数据存在性不同:缓存穿透是查询一个数据库中不存在的数据,而缓存击穿是查询一个数据库中确实存在且非常热门的数据。
    • 触发原因不同:缓存穿透是由于查询的数据在数据库中不存在,导致缓存中也没有对应的记录;缓存击穿则是由于缓存中的数据到期,而此时有大量请求需要访问这个数据。
  2. 量的区别

    • 请求量不同:缓存穿透可能涉及到的请求量相对较小,因为通常不会有很多请求同时查询一个不存在的数据;而缓存击穿则可能涉及到大量的请求,因为热门数据的访问量通常很大。
    • 影响范围不同:缓存穿透的影响范围可能较小,因为它只涉及到查询不存在的数据;而缓存击穿的影响范围可能较大,因为它涉及到的是热门数据,可能会对数据库造成较大压力。

总的来说,缓存穿透和缓存击穿在触发原因、数据存在性、请求量和影响范围等方面都存在明显的区别,这些区别既包括了质的不同,也包括了量的不同。因此,在设计缓存策略时,需要针对这两种情况分别采取不同的解决方案。


http://www.kler.cn/a/400447.html

相关文章:

  • 已有docker增加端口号,不用重新创建Docker
  • 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-04
  • c++数字雨实现
  • ChromeDriver驱动下载地址更新(保持最新最全)
  • 利用Python爬虫获取淘宝店铺详情
  • Springboot基于GIS的旅游信息管理系统
  • 初始Python篇(3)—— 列表
  • 大数据新视界 -- 大数据大厂之 Impala 性能优化:集群资源动态分配的智慧(上)(23 / 30)
  • 使用弹性方法的 BP 网络学习改进算法详解
  • 【操作系统】Linux之线程同步二(头歌作业)
  • Git 常用命令大全与详解
  • 请求响应入门
  • element ui table进行相同数据合并单元格
  • Web搭建入门教程:基于ssh向服务器推送文件
  • 了解存储过程
  • c#————委托Action使用例子
  • 【泛型 Plus】Kotlin 的加强版类型推断:@BuilderInference
  • LM2 : A Simple Society of Language Models Solves Complex Reasoning
  • MyBatis XML一个方法执行插入或更新操做(PostgreSQL)
  • Java Collection的使用
  • Jaskson处理复杂的泛型对象
  • (Linux 入门) 基本指令、基本权限
  • 动态规划 之 子序列系列 算法专题
  • 脚手架vue-cli,webpack模板
  • 资源管理功能拆解——如何高效配置和管理项目资源?
  • 高斯数据库Postgresql死锁和锁表解决方法