布隆过滤器

什么是布隆过滤器

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

哈希函数

布隆过滤器离不开哈希函数,所以在这里有必要介绍下哈希函数的概念,哈希函数的性质:

  1. 经典的哈希函数都有无限大的输入值域(无穷大)。
  2. 经典的哈希函数的输出域都是固定的范围(有穷大,假设输出域为 S)。
  3. 当给哈希函数传入相同的值时,返回值必一样。
  4. 当给哈希函数传入不同的输入值时,返回值可能一样,也可能不一样。
  5. 输入值会尽可能均匀的分布在 S 上。

前三点都是哈希函数的基础,第四点描述了哈希函数存在哈希碰撞的现象,因为输入域无限大,输出域有穷大,这是必然的,输入域中会有不同的值对应到输入域 S 中。第五点事评价一个哈希函数优劣的关键,哈希函数越优秀,分布就越均匀且与输入值出现的规律无关。比如存在 “hash1”,“hash2”,“hash3” 三个输入值比较类似,经过哈希函数计算后的结果应该相差非常大,可以通过常见的 MD5 和 SHA1 算法来验证这些特性。如果一个优秀的函数能够做到不同的输入值所得到的返回值可以均匀的分布在 S 中,将其返回值对 m 取余(%m),得到的返回值可以认为也会均匀的分布在 0~m-1 位置上。

布隆过滤器原理

布隆过滤器是一个 bit 向量或者说 bit 数组,如下图所示:

12_布隆过滤器.png

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

13_布隆过滤器.png

Ok,我们现在再存一个值 “ali”,如果哈希函数返回 3、4、8 的话,图继续变为:

14_布隆过滤器.png

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8 三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

布隆过滤器应用

比如一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。

可能很多人首先想到的会是使用 HashSet,因为 HashSet 基于 HashMap,理论上时间复杂度为:O(1)。达到了快速的目的,但是空间复杂度呢?URL 字符串通过 Hash 得到一个 Integer 的值,Integer 占 4 个字节,那 20 亿个 URL 理论上需要:

20亿*4/1024/1024/1024=7.45G 的内存,不满足空间复杂度的要求。这里就引出本文要介绍的 “布隆过滤器”。

如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路,存储位置要么是磁盘,要么是内存。很多时候要么是以时间换空间,要么是以空间换时间。

在响应时间要求比较严格的情况下,如果我们存在内里,那么随着集合中元素的增加,我们需要的存储空间越来越大,以及检索的时间越来越长,导致内存开销太大、时间效率变低。

此时需要考虑解决的问题就是,在数据量比较大的情况下,既满足时间要求,又满足空间的要求。即我们需要一个时间和空间消耗都比较小的数据结构和算法。Bloom Filter 就是一种解决方案。

布隆过滤器特点

  1. 因使用哈希判断,时间效率很高。空间效率也是其一大优势。
  2. 有误判的可能,需针对具体场景使用。
  3. 因为无法分辨哈希碰撞,所以不是很好做删除操作。

使用场景

布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。它的常用使用场景如下:

  1. 黑名单 : 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
  2. URL去重 : 网页爬虫对 URL 的去重,避免爬取相同的 URL 地址
  3. 单词拼写检查
  4. Key-Value 缓存系统的 Key 校验 (缓存穿透) : 缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及 DB 挂掉。
  5. ID 校验,比如订单系统查询某个订单 ID 是否存在,如果不存在就直接返回。

最佳实践

常见的适用常见有,利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。

另外,既然你使用布隆过滤器来加速查找和判断是否存在,那么性能很低的哈希函数不是个好选择,推荐 MurmurHash、Fnv 这些。

大Value拆分

Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此天然就可以作为布隆过滤器来使用。但是布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生成环境中建议对体积庞大的布隆过滤器进行拆分。

拆分的形式方法多种多样,但是本质是不要将 Hash(Key) 之后的请求分散在多个节点的多个小 bitmap 上,而是应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。