redis缓存穿透解决方案(布隆过滤器的实现)

布隆过滤器

1.背景

一般使用布隆过滤器来解决一个实际问题:缓存穿透。
缓存穿透:绕过 Redis 服务器,直接进入后台数据库查询的攻击方式,我们就称之为缓存穿透。缓存穿透攻击,是指恶意用户在短时内大量查询不存在的数据,导致大量请求被送达数据库进行查询,当请求数量超过数据库负载上限时,使系统响应出现高延迟甚至瘫痪的攻击行为,就是缓存穿透攻击。
而解决缓存穿透的方案通常有两种:
1.缓存空对象
从缓存上取不到数据,在数据库中也取不到,这时可以把key-value键值对写成key-null键值对,并且设置有效时间(设置短一些)。这样可以防止带有恶意的用户频繁地用一个值来攻击数据库。
redis缓存穿透解决方案(布隆过滤器的实现)_第1张图片

2.布隆过滤器

2. 介绍

过滤器,顾名思义,就是不让某些请求通过,不放行某些恶意的请求。在缓存中,采用布隆过滤器,将可能存在的数据哈希到足够大的bitmap上,如果客户端请求的数据不存在,布隆过滤器会拒绝客户端的请求,如果存在则会放行请求,再去查询缓存,按步骤执行后面的操作。
布隆过滤器主旨是采用一个很长的二进制数组,通过一系列的 Hash 函数来确定该数据是否存在。
布隆过滤器本质上是一个 n 位的二进制数组。你也知道二进制只有 0 和 1 来表示,针对于当前我们的场景。这里我模拟了一个二进制数组,其每一位它的初始值都是 0。

3. 原理

我们举个例子来感受一下其中原理。

我们提到作为当前的商城,假设有 1000 个商品编号,从 1~1000。作为布隆过滤器,在初始化的时候,实际上就是对每一个商品编号进行若干次 Hash 来确定它们的位置。

  1. “1”号商品计算:
    比如说针对于当前的“1”编号,我们对其执行了三次 Hash。所谓 Hash 函数就是将数据代入以后确定一个具体的位置。

    • Hash 1 函数:它会定位到二进制数组的索引为 1 上,并将其数值从 0 改为 1;
    • Hash 2 函数:它定位到索引为 5 的位置,并将从 0 改为 1;
    • Hash 3 函数:定位到索引为 99 的位置上,将其从 0 改为 1。
      如图:image.png
  2. “2”号商品计算:
    那 1 号商品计算完以后,该轮到 2 号商品。2 号商品经过三次 Hash 以后,分别定位到索引为 1、3 以及 98 号位置上。
    注意:原始数据中 1 号位因为刚才已经变成了 1,现在它不变;而 3 号位和 98 号位原始数据从 0 变为 1。
    这里又衍生出一个 Hash 新规则:如果在 Hash 后,原始位它是 0 的话,将其从 0 变为 1;如果本身这一位就是 1 的话,则保持不变。
    image.png
  3. “1000”号商品计算:
    此时 2 号商品也处理完了,我们继续向后 3、4、5、6、7、8 直到编号达到了最后一个 1000,当商品编号 1000 处理完后,他将索引为 3、6、98 设置为 1。
    image.png

计算完毕了,该怎么用呢?逻辑怎样呢?

  • 先看一个已经存在的情况
    比如,此时某一个用户要查询 858 号商品数据。都知道 858 是存在的,那么按照原始的三个 Hash 分别定位到了 1、5 和 98 号位,当每一个 Hash 位的数值都是 1 时,则代表对应的编号它是存在的。
    redis缓存穿透解决方案(布隆过滤器的实现)_第2张图片
  • 再看一个不存在的情况
    如这里要查询 8888。8888 这个数值经过三次 Hash 后,定位到了 3、6 和 100 这三个位置。此时索引为 100 的数值是 0,在多次 Hash 时有任何一位为 0 则代表这个数据是不存在的。
    redis缓存穿透解决方案(布隆过滤器的实现)_第3张图片
    针对以上两种情况的总结:如果布隆过滤器所有 Hash 的值都是 1 的话,则代表这个数据可能存在,它是可能存在;但如果某一位的数值是 0 的话,它是一定不存在的。

但存在一种情况,比如:

比如现在我要查询 8889 的情况,经过三次 Hash 正好每一位上都是 1。尽管在数据库中,8889 这个商品是不存在的;但在布隆过滤器中,它会被判定为存在。这就是在布隆过滤器中会出现的小概率的误判情况。

那如何减少这样的概率呢?

  • 第一个是增加二进制位数。在原始情况下我们设置索引位到达了 100,但是如果我们把它放大 1 万倍,到达了 100 万,是不是 Hash 以后的数据会变得更分散,出现重复的情况就会更小,这是第一种方式。
  • 第二个是增加 Hash 的次数。其实每一次 Hash 处理都是在增加数据的特征,特征越多,出现误判的概率就越小。
    但以上解决方案,带来一个问题:便是 CPU 需要进行更多运算,这会让布隆过滤器的性能有所降低。

redis缓存穿透--应用

redis缓存穿透解决方案(布隆过滤器的实现)_第4张图片
1. 初始化布隆过滤器

第一个部分是在应用启动时,我们去初始化布隆过滤器。例如将 1000 个、1 万个、10 万个商品进行初始化,完成从 0 到 1 的转化工作。
2. 当有请求打过来时,首先在布隆过滤器中判断数据是否存在

    不存在,直接返回
    存在,走 Redis 判断

3. 在 Redis 中存在,则返回,否则需要从数据库中获取数据,并载入 Redis 缓存中

当用户发来请求时,会附加商品编号,如果布隆过滤器判断编号存在,则直接去读取存储在 Redis 缓存中的数据;如果此时 Redis 缓存没有存在对应的商品数据,则直接去读取数据库,并将读取到的信息重新载入到 Redis 缓存中。这样下一次用户在查询相同编号数据时,就可以直接读取缓存了。

另外一种情况是,如果布隆过滤器判断没有包含编号,则直接返回数据不存在的消息提示,这样便可以在 Redis 层面将请求进行拦截。

其实在大多数情况下,我们出现误判也不会对系统产生额外的影响。因为像刚才我们设置 1% 的误判率,1 万次请求才可能会出现 100 次误判的情况。我们已经将 99% 的无效请求进行了拦截,而这些漏网之鱼也不会对我们系统产生任何实质影响。

这里继续提出一个问题:初始化后,对应商品被删怎么办?

假如布隆过滤器初始化后,对应商品被删除了,该怎么办呢?这是一个布隆过滤器的小难点。

因为布隆过滤器某一位的二进制数据,可能被多个编号的 Hash 位进行引用。比如说,布隆过滤器中 2 号位是 1,但是它可能被 3、5、100、1000 这 4 个商品编号同时引用。这里是不允许直接对布隆过滤器某一位进行删除的,否则数据就乱了,怎么办呢?

设计一个带计数的复杂数据结构:

计数布隆过滤器。在标准的布隆过滤器下,是无法得知当前某一位它是被哪些具体数据进行了引用,但是计数布隆过滤器它是在这一位上额外的附加的计数信息,表达出该位被几个数据进行了引用

你可能感兴趣的:(redis)