【Redis】聊聊缓存穿透,缓存击穿,缓存雪崩的那些事儿

前言

在我们日常的开发中,都是用数据库来进行数据的存储,大多数的系统任务中通常不会出现高并发的场景,所以看起来是没有什么问题,当涉及大数据量的需求,比如商城商品抢购的情景,或者是主页访问量较大的时候,单一使用数据库来保存数据的系统会因为面向磁盘,磁盘读写速度比较慢的问题而存在严重的性能弊端。一瞬间千千万万的请求到来,需要系统在极短的时间内完成成千上万的读写操作,这时候很容易导致数据库系统瘫痪,服务宕机等严重问题。此时Redis就很好的解决了这个问题,缓存只是为了缓解数据库的压力而添加的一层保护层,当从缓存中查询不到外面需要的数据就要去数据库查询了,但是随之而来的又是缓存穿透,缓存击穿,缓存雪崩的问题。

一. 缓存穿透

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

解决方案有以下两种:

方案1: 对于数据库中不存在的数据, 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,也对其在缓存中设置默认值Null, 为避免占用资源, 设置它的过期时间会很短,最长不超过五分钟。这个相对简单,但是也容易破解,比如攻击者通过分析数据格式,不重复的请求数据库不存在的数据,这样方案1就等于失效的。

public object getProductListNew(){
    
    
	//设置失效时间
	int cacheTime = 30;
    String cacheKey = "product_list";

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

方案2: 可以设置一些过滤规则, 最常见的就是采用布隆过滤器。它的设计思路是在数据库查询之前将数据进行过滤,如果发现数据不存在,则不再进行数据库查询,以此来减小数据库的访问压力。

布隆过滤器是一种概率型数据结构,特点就是高效的插入和查询,它会告诉你“某样东西一定不存在或者可能存在”,相对于传统的List,Set,Map等数据结构,布隆过滤器是一个bit数组,它更高效,占用空间更少,缺点就是返回的结果是概率性的,而不是确切的。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被bitmap拦截掉,从而避免了对底层存储系统的查询压力。

在这里插入图片描述
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “zhangsan” 和三个不同的哈希函数分别生成了哈希值 1、4、7

在这里插入图片描述
我们现在再存一个值 “lisi”,如果哈希函数返回 4、5、8 的话,图继续变为:

在这里插入图片描述
当我们想要判断布隆过滤器是否记录了某个数据时,布隆过滤器会先对该数据进行同样的哈希处理, 比如 “wangwu”的哈希函数返回了 2、5、8三个值,结果我们发现 2 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “wangwu” 这个数据不存在。

但是同时我们会发现,4 这个 bit 位由于”zhangsan”和”lisi”的哈希函数都返回了这个 bit 位,因此它被覆盖了。那么随着布隆过滤器保存的数据不断增多, 重复的概率就会不断增大, 所以当我们过滤某个数据时, 如果发现其三个哈希值都在过滤器中进行了记录, 那么也只能说明过滤器中可能包含了该数据, 并不能绝对肯定, 因为可能是其他数据的哈希值对结果产生了影响.这也就解释了上文所说的 布隆过滤器只能说明“某样东西一定不存在或者可能存在”.至于为什么采用三种不同的哈希函数取值, 因为三个哈希值只要有一个不存在就说明数据一定不在过滤器中, 这样做是可以减小因哈希碰撞(两个数据的哈希值相同)产生的错误概率.

二. 缓存击穿

某一个key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

使用互斥锁(mutex key)

业界比较常用的做法,是使用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;      
          }
 }```

**. 缓存雪崩**

猜你喜欢

转载自blog.csdn.net/weixin_42777004/article/details/108712476