缓存穿透?缓存雪崩?缓存击穿?布隆过滤器?我觉得我解释清除了

目录

缓存穿透

定义

解决方案

布隆过滤器是什么

布隆过滤器的缺点

快速体验布隆过滤器

哈希函数个数和布隆过滤器长度

缓存雪崩

定义

解决方案

缓存击穿

解决方案


本文将围绕下面三个知识点进行展开:

  • 缓存穿透

  • 缓存雪崩

  • 缓存击穿

缓存穿透

定义

缓存穿透:是指当查询一个一定不存在的数据时,由于缓存是在缓存不命中时被动写入的,并且出于容错考虑,如果从DB中查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要绕过缓存到DB中去查询,从而失去了缓存的意义。当访问流量过大时,会导致DB挂掉。

简而言之就是:访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉

解决方案

针对缓存穿透的问题,我们有两种比较常见的处理方法:

  1. 如果访问的key未能在DB查询到对应的值,也将空值写进缓存,但可以设置较短过期时间。 这样做的目的是为了减少不存在的数据对DB进行无效的查询,从而减少DB的压力。

  2. 采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤,从而避免了对DB的查询;

布隆过滤器是什么

布隆过滤器主要依赖一种数据结构叫做Bitmap,本质是一个位数组,它能提供一个最大长度为512MB(2^32)的位数组。

位数组就是数组的每个元素都只占用 1 bit,每个元素只能是 0 或者 1。这样申请一个 10000 个元素的位数组只占用 10000 / 8 = 1250 B 的空间,可以很好的避免性能和内存占用的问题。

布隆过滤器除了包含位数组,还有 K 个哈希函数。

布隆过滤器常用的一个功能是用来去重,也可以用来判断一个元素是否在一个集合中。当一个元素加入布隆过滤器中的时候,会进行如下操作:

  • 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。

  • 根据得到的哈希值,在位数组中把对应下标的值置为 1。

举个例子:假设布隆过滤器有 3 个哈希函数:f1, f2, f3 和一个位数组 arr。现在要把 "dashu" 插入布隆过滤器中:

  • 对值进行三次哈希计算,得到三个值 n1, n2, n3;

  • 把位数组中三个元素 arr[n1], arr[n2], arr[3] 的值设为 1

当要判断一个值是否在布隆过滤器中,对元素再次进行哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

这么听起来布隆过滤器还是可以哈!既不占内存,又可以去重,判断元素是否存在集合中啥的,看起来还挺靠谱有效的。事实上是不是真的是这样的呢?

大叔疑问句都出来了,说明这货肯定是有点古怪的......

布隆过滤器的缺点

从上面的标志方式来看,必然会出现一个问题:

当插入的元素原来越多,位数组中被置为 1 的位置就越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为 1。这样导致的问题:一个不存在布隆过滤器中的也有可能被误判成在布隆过滤器中

于是我们又可以这样总结:

  • 布隆过滤器说某个元素在,可能会被误判。

  • 布隆过滤器说某个元素不在,那么一定不在。

针对上面这种误判,是不是就放任不管了呢?不是的朋友们,redis中有两个值来决定布隆过滤器的准确率:

  • error_rate:允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大

  • initial_size:布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降

redis 中有一个命令可以来设置这两个值:

bf.reserve urls 0.01 100
  • 第一个值是过滤器的名字,

  • 第二个值为 error_rate 的值,

  • 第三个值为 initial_size 的值

快速体验布隆过滤器

redis 在 4.0 的版本中加入了 module 功能,布隆过滤器可以通过 module 的形式添加到 redis 中。需要自行编译安装:

# 下载Rebloom插件
wget https://github.com/RedisLabsModules/rebloom/archive/v1.1.1.tar.gz

# 解压 编译安装
tar zxvf v1.1.1.tar.gz
cd rebloom-1.1.1
make

# 配置redis.conf,添加如下配置,保存退出
loadmodule /xxx/redis/redis-5.0.8/RedisBloom-2.2.2/redisbloom.so

# 重启redis服务
redis-server redis.conf

# 大家客户端
redis-cli -h 127.0.0.1 -p 6379

# 测试命令
bf.add test xxx
命令成功说明开启成功

大叔这里要给大家介绍另一个比较简单的体验方法:使用docker 直接在redis中体验布隆过滤器

1、查找并下载镜像

$ docker search rebloom

$ docker pull redislabs/rebloom

2、检测下载好的镜像

$ docker images
REPOSITORY                                     TAG                   IMAGE ID            CREATED             SIZE
redislabs/rebloom                              latest                03841e395ca0        3 months ago        104MB

3、启动容器

$ docker run -d -p 6379:6379 --name bloomfilter redislabs/rebloom

4、体验布隆过滤器

$ docker exec -it bloomfilter redis-cli
127.0.0.1:6379> bf.add urls dashu
(integer) 1
127.0.0.1:6379> bf.exists urls dashu
(integer) 1
127.0.0.1:6379> bf.reserve test 0.01 100
OK
127.0.0.1:6379> bf.reserve urls 0.01 100
(error) ERR item exists
127.0.0.1:6379>
  • bf.add 添加元素到布隆过滤器中

  • bf.exists 判断某个元素是否存在过滤器中

  • bf.reserve 设置布隆过滤器允许的错误率以及可以存储元素的个数 【注意:执行这个命令之前过滤器的名字应该不存在,如果执行之前就存在会报错:(error) ERR item exists】

哈希函数个数和布隆过滤器长度

上面我们从布隆过滤器自身的属性来优化其准确性,实际上,影响布隆过滤器误报率的元素还有 哈希函数布隆过滤器长度 。它们之间存在着以下关系:

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。

显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置为 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

布隆过滤器只能插入数据判断是否存在,不能删除,而且只能保证【不存在】判断绝对准确

以上就是缓存穿透的相关内容,其中布隆过滤器的知识建议mark!

缓存雪崩

定义

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重,引起雪崩。

解决方案

可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常 “热点” 的数据。这个时候,缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

换句话说: 一个存在的热点key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

解决方案

1、使用分布式锁

简单地来说,就是在缓存失效的时候(判断拿出来的值为空),采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法,简单代码实现:

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;      
    }
}

2、“永远不过期” 这里的“永远不过期”包含两层意思:

  • 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期;

  • 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。

这种方式还是非常友好的,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据。

伪代码实现:

String get(final String key) {  
    V v = redis.get(key);  
    String value = v.getValue();  
    long timeout = v.getTimeout();  
    if (v.timeout <= System.currentTimeMillis()) {  
        // 异步更新后台异常执行  
          threadPool.execute(new Runnable() {  
              public void run() {  
                  String keyMutex = "mutex:" + key;  
                  if (redis.setnx(keyMutex, "1")) {  
                        // 3 min timeout to avoid mutex holder crash  
                        redis.expire(keyMutex, 3 * 60);  
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                  }  
              }  
          });  
    }  
    return value;  
}

以上就是今天要跟大家分享的内容,欢迎留言交流~

猜你喜欢

转载自blog.csdn.net/qq_46388795/article/details/109114241