Redis使用中缓存穿透、缓存雪崩等问题总结

735 阅读2分钟

  本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  Redis作为一种常见的非关系型数据库,因其优异的性能,在分布式系统中具备广泛的应用,通常使用Redis来实现缓存功能,基本思路如下所示:

img

  Redis作为缓存在项目中的应用极大的提升了程序的性能和效率,尤其是查询数据时。但凡事有利皆有弊,Redis缓存提高程序并发性能的同时,也带来了一些问题,如常见的缓存穿透缓存击穿以及缓存雪崩等。

缓存穿透

  何为缓存穿透?即请求查询一个在数据库中也不存在的数据导致所有的请求避开了缓存流量直接打到数据库层。一旦当这种请求量激增时,数据库压力骤增,严重时可能会导致数据库直接挂掉。

解决办法:

  • 接口层增加校验,如用户鉴权校验,请求数据的id进行基础校验,id<0这种情况的直接拦截掉;
  • 若是缓存或者是数据库中都不存在该数据,这时可以将这个keyRedis中对应的缓存设置为一个null,但是缓存有效时间设置短一点,如30s左右,以防止这个键在后面某一时刻真的有值;
  • 布隆过滤器Bloomfilter类似于一个Java中的Set集合,可用于快速判断某个元素是否在集合中,一个典型应用场景就是判读一个key是否存在于某个容器中,不存在就直接返回。布隆过滤器的关键在于hash算法和容器大小。

可能存在的问题:

  • 需要更多的键:遭受恶意攻击时,可能每次请求的key都不一样,那就会将这些key都写入到缓存当中,虽然只是一个null值,但如果数据量较大时,对于Redis的性能还是有一定影响的,所以一般在设置null值的同时也要设置过期时间;
  • Redis缓存层与DB层之间会存在短暂的数据不一致现象:因为cache层设置了过期时间,如果某一时刻,DB层存在了某key值对应的数据,但缓存层中该key对应的值仍为null,只有等过期时间过去之后,重新从DB层查询数据刷新缓存才能保持一致。

缓存击穿

  何为缓存击穿?就是在某个时刻,大量请求同时查询同一个key这个key对应的缓存正好失效了导致这些请求全部打到了DB上,此时DB有可能挂掉。

解决办法:

  • 加互斥锁,保证某一时刻只能有一个线程去进行缓存更新操作,其他线程全部等待缓存更新完成
  • 设置热点数据永不过期

互斥锁

  当从缓存中获取不到数据时,先加锁,然后从数据库中查询数据,若查到则刷新缓存,最后释放锁。如果其他线程获取锁失败,则休眠一段时间后重试。下面是使用RedissetNx来实现分布式锁,保证只有一个线程去进行缓存更新操作。

public String get(String key) throws Exception{
    String value=redisTemplate.get(key);
    if(StringUtils.isBlank(value)){
        String mutexKey="mutex:key:"+key;
        if(redisTemplate.setNx(mutexKey,"1")){
            value=getFromDB(key);
            if(StringUtils.isNotBlank(value)){
                redisTemplate.setEx(key,value,5*60);
            }
            //释放锁
            redisTemplate.del(mutexKey);
        }else{
            //其他线程休息1000ms后重试
            Thread.sleep(1000);
            get(key);
        }
    }
    return value;
}

缓存永不过期

  缓存永不过期的意思是,真正的缓存过期时间不由Redis进行控制,而是由程序代码控制。当获取数据时,发现获取数据超时,就需要发起一个异步请求去加载数据。这种策略的优点是不会产生死锁,但有可能造成缓存不一致的现象,但一般情况下是适用的。

public String get(final String key){
    V v=redisTemplate.get(key);
    String value=v.getValue();
    long logicTimeOut=v.getLogicTimeOut();
    if(logicTimeOut>=System.currentTimeMillis()){
        threadPool.execute(()-{
           String mutexKey="mutex:key:"+key;
            if(redisTemplate.setNx(mutexKey,"1")){
                vaue=getFromDB(key);
                if(StringUtils.isNotBlank(value)){
                    redisTemplate.setEx(key,value,5*60);
                }
                redisTemplate.del(mutexKey);
            }
        });
    }
    return value;
}

缓存雪崩

  缓存雪崩是指缓存中大量的key设置了相同的过期时间,导致在某个时刻这些缓存全部失效,然后所有的请求直接全部打到DB层上。与缓存击穿不同的是,击穿针对的是高并发下查询同一个key,而雪崩是高并发下不同的key都过期了,请求全部打到数据库上

解决方法:

  • 不同key的过期时间设置为随机值,这样可以避免同一时间大量key过期
  • 如果缓存是分布式部署,则将这些热点数据均匀分布到不同的缓存数据库中
  • 设置热点数据永不过期

总结

  简单介绍了Redis作为缓存使用时可能出现的问题,如缓存穿透缓存击穿缓存雪崩。在工作中使用需要注意这几个问题,保证服务的高可用。