Bloom Filter算法

Bloom Filter算法详解

什么是布隆过滤器


布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数 (下面详细说),实际上你也可以把它简单理解为一个不怎么精确的set结构,当你使用它的contains方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。

当布隆过滤器说某个值存在时,这个值可能不存在;但是当它说不存在时,那么这个值一定不存在。打个比方,当它说不认识你时,那就是真的不认识,但是当它说认识你时,可能是因为你长得像他认识的另一个朋友(脸长得有些相似),所以误判认识你。

image

布隆过滤器的使用场景


在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。

如网页URL的去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器的典型应用有:

  • 大数据判断是否存在
    如果你的服务器内存足够大的话,那么使用HashMap可能是一个不错的解决方案,理论上时间复杂度可以达到O(1)级别,但是当数据量起来之后还是只能考虑布隆过滤器。
  • 解决缓存穿透
    我们通常会把一些经常访问的数据放在Redis中当作缓存,例如产品详情。通常一个请求过来之后,我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单,也是最普遍的做法,但是如果一直请求一个不存在的缓存,那就会有大量的请求被直接打到数据库上,造成缓存穿透,布隆过滤器也可以用来解决此类问题。
  • 爬虫|邮箱等系统的过滤
    对爬虫网址进行过滤,已经存在布隆中的网址,不再爬取。
    对于垃圾邮件进行过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单内,如果在就判断为垃圾邮件。
  • 业务场景判断
    判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
  • Web拦截器
    如果是相同的请求则进行拦截,防止被重复攻击。
    用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务

为什么使用布隆过滤器


下面举一个实例说明我们为什么要学习BloomFilter

image

假设我们要写一个爬虫程序。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”,爬虫就会进入一个无限怪圈,找不到出路,程序出现崩溃。

所以为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL,也就是如何判重。

给一个URL,怎样知道蜘蛛是否已经访问过呢?按照我们的常识,就会有如下几种方案:
  1. 将访问过的URL保存到数据库,数据库管理系统可以为你去重。
  2. 用Set将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
  3. URL经过MD5SHA-1等单向哈希后再保存到Set数据库
  4. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。

方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。

以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。

方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?

方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,至少需要5GB内存,还不包括Set数据结构中的内存浪费。

方法3的缺点:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。

方法4的缺点:消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。

若要降低冲突发生的概率到1%,有种办法就是就要将BitSet的长度设置为URL个数的100倍。

假设一亿条URL,就得把BitSet长度设为100亿,过于稀疏也是很费内存的

实质上,上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!

也就是说少量URL实际上没有没被网络爬虫访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。

Bloom Filter算法原理


下面引入本篇的主角——Bloom Filter。其实上面方法4的思想已经很接近Bloom Filter了。

方法四的致命缺点是冲突概率高,为了降低冲突的概念,Bloom Filter使用了多个哈希函数,而不是一个。

为什么可以降低呢?我们知道Hash函数有一定几率出现冲突,概率假设为 p1,我们知道p1是一个很小的几率,但是在数据量大之后冲突就会变多,也就是上面第四种方法的问题。

BoomFilter使用 多个Hash函数 分别冲突概率为 p2 p3 p4 p5 … pn ,我们知道不同 Hash函数处理同一个字符串彼此独立,所以冲突概率通过乘法公式得到为: p1p2p3p4p5p6…pn,是相当相当小的了。

Bloom Filter算法如下:

预操作
创建一个 m 位 BitSet(C++自带,Python为bitarray),先将所有位初始化为0,然后选择 k 个不同的哈希函数。第 i 个哈希函数对字符串 str 哈希的结果记为h(i, str),且h(i,str)的范围是 0 到 m-1 。

image

Add操作
下面是每个字符串处理的过程,首先是将字符串str“记录”到BitSet中的过程:

对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后将BitSet的第h(1,str)、h(2,str)…… h(k,str)位设为1。

image

很简单吧?这样就将字符串str映射到BitSet中的k个二进制位了。

Check操作
根据上图,我们对每个字符串采用同样的算法。

下面是检查字符串str是否被BitSet记录过的过程:

  • 对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后检查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。
  • 若一个字符串对应的Bit不全为1,则可以肯定该字符串一定没有被Bloom Filter记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了)
  • 但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为wrong position。

Delete操作
字符串加入了就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用Counting bloomfilter(CBF),这是一种基本Bloom Filter的变体,CBF将基本Bloom Filter每一个Bit改为一个计数器,这样就可以实现删除字符串的功能了。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

Bloom Filter 优化


image

考虑到BoomFilter上面的指标,总结一下有以下几个

m : BitSet 位数

n : 插入字符串个数

k :hash函数个数

当然,哈希函数也是影响的重要因素

从表格来看 m/n越大越准,k越大越准。

但是具体怎么设计呢?

哈希函数选择

  • 哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。
  • 选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。

参数设计
相信大家对于 Bloom Filter 的工作原理都有了一个基本的了解,现在我们来看看在Bloom Filter 中涉及到的一些参数指标:

  • 欲插入Bloom Filter中的元素数目: n
  • Bloom Filter误判率: P(true)
  • BitArray数组的大小: m
  • Hash Function的数目: k

欲插入Bloom Filter中的元素数目 n 是我们在实际应用中可以提前获取或预估的;Bloom Filter的误判率 P(true) 则是我们提前设定的可以接受的容错率。所以在设计Bloom Filter过程中,最关键的参数就是BitArray数组的大小 m 和 Hash Function的数目 k,下面将给出这两个关键参数的设定依据、方法

误判率P(true)

image

Hash Function的数目k

前文已经看到Hash Function数目k的增加可以减小误判率P(true),但是随着Hash Function数目k的继续增加,反而会使误判率P(true)上升,即误判率是一个关于Hash Function数目k的凸函数。所以当k在极值点时,此时误判率即为最小值

image

image

BitArray数组的大小 m

image

如何解决布隆过滤器不支持删除的问题


(1)Counting Bloom Filter
Counting Bloom Filter将标准 Bloom Filter位数组的每一位扩展为一个小的计数器(counter),在插入元素时给对应的k(k为哈希函数个数)个Counter的值分别加1,删除元素时给对应的k个Counter的值分别减1。Counting Bloom Filter通过多占用几倍的存储空间的代价,给Bloom Filter增加了删除操作。

image

(2)布谷鸟过滤器
对于这种方式有兴趣的读者可以阅读这篇文章://juejin.cn/post/6924636027948630029#heading-1

Python 代码简单实现


主体

from bitarray import bitarray # 产生BitSet

import mmh3 # 产生Hash函数
 
 
class BloomFilter(set):
 
    def __init__(self, size, hash_count):
        super(BloomFilter, self).__init__()
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)
        self.size = size
        self.hash_count = hash_count
 
    def __len__(self):
        return self.size
 
    def __iter__(self):
        return iter(self.bit_array)
 
    def add(self, item):
        for seed in range(self.hash_count):
            index = mmh3.hash(item, seed) % self.size
            self.bit_array[index] = 1
 
        return self
 
    def __contains__(self, item):
        out = True
        for seed in range(self.hash_count):
            index = mmh3.hash(item, seed) % self.size
            if self.bit_array[index] == 0:
                out = False
 
        return out

测试

def main():
    bloom = BloomFilter(10000, 20)
    animals = ['dog', 'cat', 'giraffe', 'fly', 'mosquito', 'horse', 'eagle',
               'bird', 'bison', 'boar', 'butterfly', 'ant', 'anaconda', 'bear',
               'chicken', 'dolphin', 'donkey', 'crow', 'crocodile']
    # First insertion of animals into the bloom filter
    for animal in animals:
        bloom.add(animal)
 
    # Membership existence for already inserted animals
    # There should not be any false negatives
    for animal in animals:
        if animal in bloom:
            print('{} is in bloom filter as expected'.format(animal))
        else:
            print('Something is terribly went wrong for {}'.format(animal))
            print('FALSE NEGATIVE!')
 
    # Membership existence for not inserted animals
    # There could be false positives
    other_animals = ['badger', 'cow', 'pig', 'sheep', 'bee', 'wolf', 'fox',
                     'whale', 'shark', 'fish', 'turkey', 'duck', 'dove',
                     'deer', 'elephant', 'frog', 'falcon', 'goat', 'gorilla',
                     'hawk' ]
    for other_animal in other_animals:
        if other_animal in bloom:
            print('{} is not in the bloom, but a false positive'.format(other_animal))
        else:
            print('{} is not in the bloom filter as expected'.format(other_animal))
 
 
if __name__ == '__main__':
    main()

结果

dog is in bloom filter as expected
cat is in bloom filter as expected
giraffe is in bloom filter as expected
fly is in bloom filter as expected
mosquito is in bloom filter as expected
horse is in bloom filter as expected
eagle is in bloom filter as expected
bird is in bloom filter as expected
bison is in bloom filter as expected
boar is in bloom filter as expected
butterfly is in bloom filter as expected
ant is in bloom filter as expected
anaconda is in bloom filter as expected
bear is in bloom filter as expected
chicken is in bloom filter as expected
dolphin is in bloom filter as expected
donkey is in bloom filter as expected
crow is in bloom filter as expected
crocodile is in bloom filter as expected
badger is not in the bloom filter as expected
cow is not in the bloom filter as expected
pig is not in the bloom filter as expected
sheep is not in the bloom, but a false positive
bee is not in the bloom filter as expected
wolf is not in the bloom filter as expected
fox is not in the bloom filter as expected
whale is not in the bloom filter as expected
shark is not in the bloom, but a false positive
fish is not in the bloom, but a false positive
turkey is not in the bloom filter as expected
duck is not in the bloom filter as expected
dove is not in the bloom filter as expected
deer is not in the bloom filter as expected
elephant is not in the bloom, but a false positive
frog is not in the bloom filter as expected
falcon is not in the bloom filter as expected
goat is not in the bloom filter as expected
gorilla is not in the bloom filter as expected
hawk is not in the bloom filter as expected

从输出结果可以发现,存在不少误报样本,但是并不存在假阴性。

不同于这段布隆过滤器的实现代码,其它语言的多个实现版本并不提供哈希函数的参数。这是因为在实际应用中误报比例这个指标比哈希函数更重要,用户可以根据误报比例的需求来调整哈希函数的个数。通常来说,sizeerror_rate是布隆过滤器的真正误报比例。如果你在初始化阶段减小了error_rate,它们会调整哈希函数的数量。

参考资料


//cloud.tencent.com/developer/article/1731494 
//blog.csdn.net/a745233700/article/details/113751718
//juejin.cn/post/6924636027948630029#heading-1
//www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/#%E4%B8%80%E3%80%81%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8%E7%AE%80%E4%BB%8B
//segmentfault.com/a/1190000024566947
//github.com/jaybaird/python-bloomfilter
//blog.csdn.net/weixin_42081389/article/details/103137671
Tags: