为减少数据库的访问频率和降低数据库的压力,所以需加入 Redis 用作缓存。但是当 Redis 不存在此数据的时候,那请求还是会直达数据库的,且同一时间内的缓存失效量过多或者是被恶意攻击的时候,都会增加数据库的压力,那应该如何防止呢?
缓存雪崩
Redis 中同一时间内的大批量缓存失效称之为缓存雪崩,如果这段时间还存在大量的请求发起,那就会导致数据库内出现大量的请求,会直接冲垮数据库。
数据库里存在,而缓存中没有,数据是因为时间失效而直达数据库的,这种情况称之为缓存雪崩。
解决缓存雪崩的常用方法如下:
1
通过加锁的方法,保证单线程访问缓存,避免过多的请求到达数据库。
2
key 值的失效时间应该设置不同,例如初始化预热数据<typo data-origin="值的" ignoretag="true">的时</typo>候,可以确保数据缓存的时候是不同时间进入的,直接避免很多缓存失效。
3
缓存想要设置成永不失效是在内存允许的情况下进行的。
缓存击穿
缓存击穿类似于缓存雪崩,唯一不同的是缓存击穿是指某个缓存失效,且同一时间有很大的并发请求访问这个 key,增加了数据库的缓存压力。
解决缓存击穿的方法类似于解决缓存雪崩的方法:
1
加锁,保证单线访问缓存。这样第一个请求到达数据库后就会重新写入缓存,后续的请求就可以直接读取缓存。
2
内存允许的情况下,可以将缓存设置为永不失效。
缓存穿透
缓存穿透是访问数据既不存在于 Redis 中,也不存在于数据库中。当达到一定的数量时就会导致数据库中进入大量的数据,给数据库的存储带来挑战。
解决方案:
1
加锁在缓存穿透中解决不了太大的问题,因为本身 key 就是虚拟存在的,所以需要控制<typo data-origin="地"
2
ignoretag="true">线</typo>程的访问数,但还是无法阻止请求进入到数据库。
结合以下几种方案解决缓存穿透问题:
1
接口层进行校验,发现非法的 key 直接返回。比如数据库中采用的是自增 id,那么如果来了一个非整型的 id 或者负数 id 可以直接返回,或者说如果采用的是 32 位 uuid,那么发现 id 长度不等于 32 位也可以直接返回。
2
缓存不存在的数据,可以缓存一个空或者是其他约定好的无效value。其中 key 的设置需要是短期失效,否则将会导致大量的key 进入到 Redis 中。
布隆过滤器(Bloom Filter)
缓存穿透的解决方案中我们可以假设每个 key 都是第二种方式的话,那将会有不存在的 key 大量的被访问,将会全部进入到内存中被存储,此方式试行性不高。
什么好的解决方案吗?布隆过滤器,它会以最小的空间存储更多的数据。
什么是布隆过滤器?
1970年布隆提出了布隆过滤器(Bloom Filter),布隆过滤器(Bloom Filter)是二进制向量(位图)和随机映射函数(哈希函数)。布隆过滤器(Bloom Filter)主要是检索元素是否存在与集合内。优点是比一般的算法在空间效率和查询时间上都要好,缺点是存在误识率且难删除。
位图(Bitmap):
位图是 Redis 中的数据结构,位图的实现形式是为数组,这也是布隆过滤器的重要实现形式。在这个数组中每个位置只有两种状态,即 0 和 1 。每个元素只占用 1 个字节,其中 0 表示没有元素存在,1 表示有元素存在。如下图所示就是一个简单的布隆过滤器示例(一个 key 值经过哈希运算和位运算就可以得出应该落在哪个位置):

哈希碰撞
lonely 和 wolf 落于同一个位置,在哈希运算的结果下不同的 key 值获得相同值的现象被称之为哈希碰撞,然后再位运算的方式,最后会让其落在同一位置。但哈希碰撞的量过多,也会引起数据判断的不准确性。
所以,为减少哈希碰撞,以下两个因素是我们需要综合考虑的:
1
增大位图数组的大小(位图数组和占用的内存呈正比)。
2
增加哈希函数的次数(同一个 key 值经过 1 个函数相等了,那么经过 2 个或者更多个哈希函数的计算,都得到相等结果的概率就自然会降低了)。
上面两个方法我们需要综合考虑:
比如增大位数组,那么就需要消耗更多的空间,而经过越多的哈希计算也会消耗 cpu 影响到最终的计算时间,所以位数组到底多大,哈希函数次数又到底需要计算多少次合适需要具体情况具体分析。
布隆过滤器的 2 大特点:
下图这个就是一个经过了 2 次哈希函数得到的布隆过滤器,根据下图我们很容易看到,假如我们的 Redis 根本不存在,但是 Redis 经过 2 次哈希函数之后得到的两个位置已经是 1 了(一个是 wolf 通过 f2 得到,一个是 Nosql 通过 f1 得到,这就是发生了哈希碰撞,也是布隆过滤器可能存在误判的原因)。

上述现象中以布隆过滤器的角度得出其具备的两大特点:
布隆过滤器判断某元素存在,此元素可能存在。
布隆过滤器判断某元素不存在,此元素肯定不存在。
而从元素的角度也可得出其具备的两大特点:
某元素存在,那布隆过滤器会判断其存在。
某元素不存在,那布隆过滤器可能会判断其存在。
PS:需要注意的是,如果经过 N 次哈希函数,则需要得到的 N 个位置都是 1 才能判定存在,只要有一个是 0,就可以判定为元素不存在布隆过滤器中。
fpp
因为布隆过滤器中总是会存在误判率,因为哈希碰撞是不可能百分百避免的。布隆过滤器对这种误判率称之为假阳性概率,即:False Positive Probability,简称为 fpp。
在实践中使用布隆过滤器时可以自己定义一个 fpp,然后就可以根据布隆过滤器的理论计算出需要多少个哈希函数和多大的位数组空间。需要注意的是这个 fpp 不能定义为 100%,因为无法百分保证不发生哈希碰撞。
布隆过滤器的实现(Guava)
在 Guava 的包中提供了布隆过滤器的实现,下面就通过 Guava 来体会一下布隆过滤器的应用:
引入 pom 依赖
新建一个测试类 BloomFilterDemo:
运行之后的结果为:

image
第一部分输出的 mightContainNum1一定是和 for 循环内的值相等,也就是百分百匹配。即满足了原则 1:如果元素实际存在,那么布隆过滤器一定会判断存在。
第二部分的输出的误判率即 fpp 总是在 3% 左右,而且随着 for 循环的次数越大,越接近 3%。即满足了原则 2:如果元素不存在,那么布隆过滤器可能会判断存在。
这个 3% 的误判率是如何来的呢?我们进入创建布隆过滤器的 create 方法,发现默认的fpp就是 0.03:

image
对于这个默认的 3% 的 fpp 需要多大的位数组空间和多少次哈希函数得到的呢?在 BloomFilter 类下面有两个 default 方法可以获取到位数组空间大小和哈希函数的个数:
optimalNumOfHashFunctions:获取哈希函数的次数
optimalNumOfBits:获取位数组大小
debug 进去看一下:
[图片上传失败...(image-ffc4cf-1614312370626)]
得到的结果是 7298440 bit=0.87M,然后经过了 5 次哈希运算。可以发现这个空间占用是非常小的,100W 的 key 才占用了 0.87M。
PS:点击这里可以进入网站计算 bit 数组大小和哈希函数个数。
布隆过滤器的如何删除
布隆过滤器判断一个元素存在就是判断对应位置是否为 1 来确定的,但是如果要删除掉一个元素是不能直接把 1 改成 0 的,因为这个位置可能存在其他元素,所以如果要支持删除,那我们应该怎么做呢?最简单的做法就是加一个计数器,就是说位数组的每个位如果不存在就是 0,存在几个元素就存具体的数字,而不仅仅只是存 1,那么这就有一个问题,本来存 1 就是一位就可以满足了,但是如果要存具体的数字比如说 2,那就需要 2 位了,所以带有计数器的布隆过滤器会占用更大的空间。
带有计数器的布隆过滤器
下面就是一个带有计数器的布隆过滤器示例:
pom 文件引入依赖:
新建一个带有计数器的布隆过滤器 CountingBloomFilter:
构建布隆过滤器前面 2 个参数一个就是期望的元素数,一个就是 fpp 值,后面的 countingBits 参数就是计数器占用的大小,这里传了一个 8 位,即最多允许 255 次重复,如果不传的话这里默认是 16 位大小,即允许 65535次重复。
总结
本文主要讲述了使用 Redis 存在的三种问题:缓存雪崩,缓存击穿和缓存穿透。并分别对每种问题的解决方案进行了描述,最后着重介绍了缓存穿透的解决方案:布隆过滤器。原生的布隆过滤器不支持删除,但是可以引入一个计数器实现带有计数器的布隆过滤器来实现删除功能,同时在最后也提到了,带有计数器的布隆。
注:文章来源于网络。
如有侵权,请于后台联系,做删除处理,感谢您的支持。

