实用的算法之布隆过滤

2023年 1月 4日 19.6k 0

1. 什么是布隆过滤

布隆过滤(Bloom Filter)是布隆在 1970 年提出的一种数据结构。将元素(x、y、z)通过一系列函数,映射到一个二进制向量(0101 序列),用于快速判断元素 w 是否在一个集合当中。如下图(来自维基百科):相较于使用单个映射函数,在相同的地址空间下,多个映射函数能降低冲突率。因此,在相同冲突率下,多个映射函数比单个映射函数需要的地址空间更少。Bloom Filter 使用很短的二进制向量,通过牺牲准确率获得了极高的空间利用率。在大规模数据查询场景下,能有效避免磁盘 IO,具有很高地查询效率。其实,用于判断一个元素是否在一个大集合中,还可以使用 Bitmap 。将元素转换成整数类型 x ,x 在 Bitmap 中的索引值为 0(代表不存在) 或者 1(代表存在)。Bloom Filter 和 Bitmap 有所类似,都是利用二进制向量和映射函数判断元素是否存在。不同的是,Bitmap 只有一个映射函数,向量大小不能小于最大的整数;Bloom Filter 具有多个映射函数,根据不同要求的误判率场景,可以选择不同大小的二进制向量。

2. 常见的应用场景

Bloom Filter 具有一定的误判率,主要解决一定不存在和可能存在两类问题。

2.1 一定不存在问题 - False

  • 单词拼写检查。拼写错误的单词一定不存在
  • 防止数据库穿库。查询不存在的行或者列
  • 缓存穿透。没查询到缓存时,穿透到数据库

2.2 可能存在 - True

  • 爬虫对 URL 去重。跳过已经抓取的 URL
  • 垃圾邮件过滤。黑名单中的地址将被屏蔽
  • 避免重复推荐文章。跳过已经阅读的文章 URL/ID
  • Web 拦截器。拦截在黑名单中的 URL 地址

3. 布隆过滤的参数选择

上面提到 ,根据不同要求的误判率场景,布隆过滤可以选择不同大小的二进制向量。在生产过程中,我们需要平衡误判率和效率。下面是维基百科中给出的公式:k = (m/n) ln2其中,

  • m 是二进制向量的大小
  • n 是元素的数量
  • k 是映射函数的个数
  • ln2 是常数,约等于 0.69

下表是不同 m/n 和 k 下的误判率。

m/nkk=1k=2k=3k=4k=5k=6k=7k=8
2 1.39 0.393 0.400            
3 2.08 0.283 0.237 0.253          
4 2.77 0.221 0.155 0.147 0.160        
5 3.46 0.181 0.109 0.092 0.092 0.101      
6 4.16 0.154 0.0804 0.0609 0.0561 0.0578 0.0638    
7 4.85 0.133 0.0618 0.0423 0.0359 0.0347 0.0364    
8 5.55 0.118 0.0489 0.0306 0.024 0.0217 0.0216 0.0229  
9 6.24 0.105 0.0397 0.0228 0.0166 0.0141 0.0133 0.0135 0.0145
10 6.93 0.0952 0.0329 0.0174 0.0118 0.00943 0.00844 0.00819 0.00846
11 7.62 0.0869 0.0276 0.0136 0.00864 0.0065 0.00552 0.00513 0.00509
12 8.32 0.08 0.0236 0.0108 0.00646 0.00459 0.00371 0.00329 0.00314
13 9.01 0.074 0.0203 0.00875 0.00492 0.00332 0.00255 0.00217 0.00199
14 9.7 0.0689 0.0177 0.00718 0.00381 0.00244 0.00179 0.00146 0.00129
15 10.4 0.0645 0.0156 0.00596 0.003 0.00183 0.00128 0.001 0.000852
16 11.1 0.0606 0.0138 0.005 0.00239 0.00139 0.000935 0.000702 0.000574
17 11.8 0.0571 0.0123 0.00423 0.00193 0.00107 0.000692 0.000499 0.000394
18 12.5 0.054 0.0111 0.00362 0.00158 0.000839 0.000519 0.00036 0.000275
19 13.2 0.0513 0.00998 0.00312 0.0013 0.000663 0.000394 0.000264 0.000194
20 13.9 0.0488 0.00906 0.0027 0.00108 0.00053 0.000303 0.000196 0.00014
21 14.6 0.0465 0.00825 0.00236 0.000905 0.000427 0.000236 0.000147 0.000101
22 15.2 0.0444 0.00755 0.00207 0.000764 0.000347 0.000185 0.000112 7.46e-05
23 15.9 0.0425 0.00694 0.00183 0.000649 0.000285 0.000147 8.56e-05 5.55e-05
24 16.6 0.0408 0.00639 0.00162 0.000555 0.000235 0.000117 6.63e-05 4.17e-05
25 17.3 0.0392 0.00591 0.00145 0.000478 0.000196 9.44e-05 5.18e-05 3.16e-05
26 18 0.0377 0.00548 0.00129 0.000413 0.000164 7.66e-05 4.08e-05 2.42e-05
27 18.7 0.0364 0.0051 0.00116 0.000359 0.000138 6.26e-05 3.24e-05 1.87e-05
28 19.4 0.0351 0.00475 0.00105 0.000314 0.000117 5.15e-05 2.59e-05 1.46e-05
29 20.1 0.0339 0.00444 0.000949 0.000276 9.96e-05 4.26e-05 2.09e-05 1.14e-05
30 20.8 0.0328 0.00416 0.000862 0.000243 8.53e-05 3.55e-05 1.69e-05 9.01e-06
31 21.5 0.0317 0.0039 0.000785 0.000215 7.33e-05 2.97e-05 1.38e-05 7.16e-06
32 22.2 0.0308 0.00367 0.000717 0.000191 6.33e-05 2.5e-05 1.13e-05 5.73e-06

在使用时,首先确定一个可接受的误判率,然后根据公式计算出二进制向量的大小:m = (k * n) / ln2例如,选择 0.003 的误判率,k = 4,m/n = 15 。如果有 100 W 条记录,那么需要二进制向量大小为 ( 4 * 10e6 ) / 0.693 ≈ 5.77 * 10e6 bit = 704.6 KB 。

4. 布隆过滤的缺点

布隆过滤的主要缺点:

  • 无法删除元素。因为一个二进制向量位,可能对应多个元素的映射,不能直接将其置 0 。
  • 只适用于单机系统,内存开销随数据规模成线性关系。目前已经有一些中间件提供了布隆过滤,比如 Redis ,可以用于超大规模数据的场景。
  • 对布隆过滤的优化算法很多,无非就是增加信息的冗余,但从效率上都比不上布隆过滤。下面是几种同类算法:

    • 计数布隆过滤(Counting Bloom Filter)

    在标准布隆过滤的基础上,将每一个 Bit 改为一个计数器,增加一个元素时,计数加一,删除一个元素时,计数减一。

    • Spectral Bloom Filters

    上面的计数布隆过滤,计数器是固定位数的。Spectral Bloom Filters 的计数位是动态变化的,更加灵活,避免计数溢出。

    • 压缩布隆过滤(Compressed Bloom Filters)

    通过减少映射函数的数量,减少网络传输的 Bit 位。为了换取相同的误判率,二进制向量将会变大。

    • D-left 计算布隆过滤(D-left Counting Bloom Filters)

    基于 D-left Hashing ,减少了存储空间,降低了误判率,可以删除元素。

    • Dynamic Counting Filters

    支持查询元素的存储频率

    • 布谷鸟过滤

    布谷鸟算法不同于布隆过滤,而是模仿布谷鸟解决映射冲突问题。当不同元素因素到同一个映射位时,最后映射的元素会踢走之前映射的元素。布谷鸟算法支持删除操作,空间利用率只有 50 %,只存储元素的指纹信息,查询效率很高。

    5. Go 语言实现

    这里引用 github.com/willf/bloom ,对 Bloom Filter 进行简单测试。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package main
    
    import (
    	"fmt"
    	"github.com/willf/bloom"
    )
    
    func main() {
    	n := uint(10000)
    	error_rate := 0.003
    	need_m, need_k := bloom.EstimateParameters(n, error_rate)
    	fmt.Printf("Set m = %d , k = %d \n", need_m, need_k)
    	filter := bloom.New(need_m, need_k)
    	for i := 0; i < int(n); i++ {
    		filter.Add([]byte(fmt.Sprintf("https://www.chenshaowen.com/%d", i)))
    	}
    	fmt.Println(filter.Test([]byte(fmt.Sprintf("https://www.chenshaowen.com/%d", 10000))))
    	fmt.Printf("Done")
    }
    

    执行结果:

    1
    2
    3
    
    Set m = 120910 , k = 9 
    false
    Done
    

    6. 参考

    • http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
    • https://en.wikipedia.org/wiki/Bloom_filter
    • https://github.com/willf/bloom

    相关文章

    KubeSphere 部署向量数据库 Milvus 实战指南
    探索 Kubernetes 持久化存储之 Longhorn 初窥门径
    征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
    那些年在 Terraform 上吃到的糖和踩过的坑
    无需 Kubernetes 测试 Kubernetes 网络实现
    Kubernetes v1.31 中的移除和主要变更

    发布评论