应对缓存穿透问题,我们可以使用布隆过滤器。我们先来回顾下缓存穿透知识点哈:

一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

假设我们需要查产品详情,有查询请求进来,我们先根据产品 Id 直接去缓存中查一下,没有的话,再去查下数据库。如果现在有大量请求进来,而且都在请求一个不存在的产品 Id,那么这些请求都会怼到数据库,数据库压力一上来,可能就挂了。我们可以在请求数据库层前,加个中间层,去缓解数据库压力嘛,如果,不存在的话,就不去查数据库啦。

这个中间层,是不是用 HashMap 就好了呢?听起来不错嘛,HashMap 时间复杂度可以达到 O (1),但是呢因为 HashMap 数据是在内存里面的,如果大量的数据远超出了服务器的内存呢,那就无法使用 HashMap 啦,可以使用布隆过滤器来做这个缓冲的事情。

布隆过滤是什么
布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组 Hash 映射函数组成,它用于检索一个元素是否在一个集合中,空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

布隆过滤器原理是? 假设我们有个集合 A,A 中有 n 个元素。利用 k 个哈希散列函数,将 A 中的每个元素映射到一个长度为 a 位的数组 B 中的不同位置上,这些位置上的二进制数均设置为 1。如果待检查的元素,经过这 k 个哈希散列函数的映射后,发现其 k 个位置上的二进制数全部为 1,这个元素很可能属于集合 A,反之,一定不属于集合 A。

来看个简单例子吧,假设集合 A 有 3 个元素,分别为 {d1,d2,d3}。有 1 个哈希函数,为 Hash1。现在将 A 的每个元素映射到长度为 16 位数组 B。

 

我们现在把 d1 映射过来,假设 Hash1(d1)= 2,我们就把数组 B 中,下标为 2 的格子改成 1,如下:

 

我们现在把 d2 也映射过来,假设 Hash1(d2)= 5,我们把数组 B 中,下标为 5 的格子也改成 1,如下:

 

接着我们把 d3 也映射过来,假设 Hash1(d3)也等于 2,它也是把下标为 2 的格子标 1:

因此,我们要确认一个元素 dn 是否在集合 A 里,我们只要算出 Hash1(dn)得到的索引下标,只要是 0,那就表示这个元素不在集合 A,如果索引下标是 1 呢?那该元素可能是 A 中的某一个元素。因为你看,d1 和 d3 得到的下标值,都可能是 1,还可能是其他别的数映射的,布隆过滤器是存在这个缺点的:会存在 hash 碰撞导致的假阳性,判断存在误差。

如何减少这种误差呢?

搞多几个哈希函数映射,降低哈希碰撞的概率
同时增加 B 数组的 bit 长度,可以增大 hash 函数生成的数据的范围,也可以降低哈希碰撞的概率
我们又增加一个 Hash2 哈希映射函数,假设 Hash2(d1)=6,Hash2(d3)=8, 它俩不就不冲突了嘛,如下:

 

即使存在误差,我们可以发现,布隆过滤器并没有存放完整的数据,它只是运用一系列哈希映射函数计算出位置,然后填充二进制向量。如果数量很大的话,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是挺划算的。

目前布隆过滤器已经有相应实现的开源类库啦,如 Google 的 Guava 类库,Twitter 的 Algebird 类库,信手拈来即可,或者基于 Redis 自带的 Bitmaps 自行实现设计也是可以的。