Redis缓存穿透、缓存击穿、缓存雪崩

1、背景

一般企业都会用到mysql等关系型数据库,当访问量不大的时候还可以支撑;当并发量高的时候,比如商品抢购或者主页访问瞬间较大的时候,请求直接到达db,可能会导致系统性能急剧下降以致瘫痪。db是面向磁盘的,磁盘IO是比较重的操作,性能较低。为了克服上述的问题,通常需要在客户端和db之间引入一层缓存NoSql技术,这是一种基于内存的数据库,并提供一定的持久化功能,比如redis。但是引入缓存之后,又有可能出现缓存穿透等问题。

2、概念

缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

3、解决方案

1、缓存穿透

查询一个一定不存在的数据,缓存不命中,最终达到db,查询不到数据又不写入缓存,导致每次都要查db,失去缓存的意义。
方案:

  1. 布隆过滤器,维护一个足够大的bitmap,不存在的key一定会被拦截到,降低对底层db的查询压力
  2. 简单粗暴的方法,如果从db重查询为空,则缓存一个默认的值代表空值,过期时间设置较短些,最长不超过5min

粗暴方式的伪代码:

//伪代码
    public object getValue() {
        int cacheTime = 30;
        String cacheKey = "key";

        String cacheValue = RedisHelper.Get(cacheKey);
        if (cacheValue != null) {
            return cacheValue;
        }

        cacheValue = RedisHelper.Get(cacheKey);
        if (cacheValue != null) {
            return cacheValue;
        } else {
            //数据库查询不到,为空
            cacheValue = getValueFromDb();
            if (cacheValue == null) {
                //如果发现为空,设置个默认值,也缓存起来
                cacheValue = string.Empty;
            }
            RedisHelper.Add(cacheKey, cacheValue, cacheTime);
            return cacheValue;
        }
    }

2、缓存击穿

热点key(某些时间点被超高并发访问)失效,请求直达db。
方案:

  1. 设置永不过期,另起调度任务定时更新key的值
  2. 业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

互斥锁伪代码:

public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表缓存值过期
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
      if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                      sleep(50);
                      get(key);  //重试
              }
          } else {
              return value;      
          }
 }

3、缓存雪崩

与缓存击穿的区别在于这里针对多个key,缓存击穿针对一个key。
方案:

  1. 加锁排队:减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
  2. 设置过期标志更新缓存
  3. 为key设置不同的随机的缓存失效时间

设置过期标志更新缓存伪代码

public object get() {
        int cacheTime = 30;
        String cacheKey = "product_list";
        //缓存标记
        String cacheSign = cacheKey + "_sign";

        String sign = RedisHelper.Get(cacheSign);
        //获取缓存值
        String cacheValue = RedisHelper.Get(cacheKey);
        if (sign != null) {
            return cacheValue; //未过期,直接返回
        } else {
            RedisHelper.Add(cacheSign, "1", cacheTime);
            ThreadPool.QueueUserWorkItem((arg) -> {
                //这里一般是 sql查询数据
                cacheValue = GetProductListFromDB();
                //日期设缓存时间的2倍,用于脏读
                RedisHelper.Add(cacheKey, cacheValue, cacheTime * 2);
            });
            return cacheValue;
        }
    }

解释说明:

  • 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
  • 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

4、总结

针对业务系统,永远都是具体情况具体分析;一切不以业务为基础的框架设计都是耍流氓,所以还是要具体问题具体分析,找出一个最合适的方案。
参考:
https://www.cnblogs.com/xichji/p/11286443.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值