一、背景介绍
在互联网中,我们经常遇到需要在大量数据中判断目标数据是否存在的情况。例如,在网络爬虫中,我们需要判断某个网址是否已经被访问过。为了实现这一功能,通常需要使用一个容器来存储已访问过的网址。如果将这些数据直接存储在磁盘中,每次判断都要进行磁盘查询,这将导致大量的IO操作,效率较低。因此,我们希望将这些数据保存在内存中。在数据量较小的情况下,可以使用Redis来存储这些数据。但是,当数据量超过上千万时,将会消耗几GB甚至几十GB的内存空间。然而,对于仅需要记录数据是否存在的情况而言,这样使用大量内存显然是浪费的。为了解决这个问题,我们可以使用布隆过滤器(Bloom Filter)。布隆过滤器是一种占用空间少且时间效率高的工具。
二、认识布隆过滤器
2.1 布隆过滤器简介
布隆过滤器(Bloom Filter)是1970年由布隆提出的,它实质上是一个很长的二进制向量和一系列随机映射函数 (Hash函数)。
作用:它是一个空间效率高的概率型数据结构,用来告诉你:一个元素一定不存在或者可能存在。
2.2 优点
- 相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即hash函数的个数)。
- Hash 函数相互之间没有关系,方便由硬件并行实现。
- 布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
- 布隆过滤器可以表示全集,其它任何数据结构都不能。
2.3 缺点
- 有误判率存在。
- 不支持删除。
2.4 适用场景
- 预防缓存穿透:布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。
- 网络爬虫:布隆过滤器可以用来去重已经爬取过的URL。
- 邮箱的垃圾邮件过滤。
- 黑白名单。
三、 布隆过滤器原理
3.1 结构
布隆过滤器实现原理就是一个超大位数的数组和多个不同Hash算法函数。假设位数组的长度为 m,哈希函数的个数为 k。如下图,一个长度16位的数组,3个不同Hash算法函数,数组里面存储的是 bit 位,只放 0 和 1,初始为 0。
不同Hash算法函数
指定长度数组
3.2 添加元素
将要添加的元素分别通过k个哈希函数计算得到k个哈希值,这k个hash值对应位数组上的k个位置,然后将这k个位置设置为1。
我们添加一个data1和data2两个元素,两个元素根据三个hash算法函数计算出的值,需要说明一点三个值可能会存在相同的值。
其中data1计算出1、8、13三值,我们把数组中对应的位置设置为1。
Hash1(data1)=1
Hash2(data1)=8
Hash3(data1)=13
如图:
data2计算出2、5、13三值,我们把数组中对应的位置设置为1
Hash1(data2)=2
Hash2(data2)=5
Hash3(data2)=13
如图:
我们发现data1和data2经过hash函数后,出现了一个相同值,这种是正常的,也正是因为这种情况的存在,需要多个函数来保证每个元素尽可能对应数组位置的唯一性,可以看下两个元素在一起的效果。
如图:
当不同元素在不同或者相同的hash函数计算后,得到同一个值,依旧只需要这个位置保持1即可。
3.3 查询元素
将要查询的元素分别通过k个哈希函数计算得到k个哈希值,这k个hash值对应位数组上的k个位置。如果这k个位置中有一个位置为0,则此元素一定不存在集合中。如果这k个位置全部为1,则这个元素可能存在。
我们在刚才添加过data1和data2两个元素的布隆过滤器查询以下三种元素,data1已添加到布隆过滤器元素,data3和data4都是未添加到布隆过滤器元素。
查询data1先根据添加时的三个hash函数计算分别对应值,值分别是1、8、13,然后查询数组中这三个位置的值是否为1。
Hash1(data1)=1
Hash2(data1)=8
Hash3(data1)=13
如图:
我们可以看到数组中1、8、13这三个位置都是1,data1可能存在于该布隆过滤器。我们从添加的角度来看,我们知道data1是一定存在于该布隆过滤器的,为什么还要是说可能呢,是因为查询出来三个位置都为1不能代表这个三个1都是同一个元素添加的,下面我们看下元素data3的查询。
查询data3先根据添加时的三个hash函数计算分别对应值,值分别是2、8、13,然后查询数组中这三个位置的值是否为1。
Hash1(data3)=2
Hash2(data3)=8
Hash3(data3)=13
如图:
我们已知的该布隆过滤器我们没有添加给data3,为什么data3查询出来三个位置的值都为1呢。我们可以看到data3所命中的位置分别是data2添加时把位置2赋值的1,和data1添加时把位置8和位置13赋值的1,都是由其他元素改变的位置对应的值,所以命中位置全部为1。这个元素可能存在。
我们查询一下data4,看下命中位置不全为0的数据。查询data4先根据添加时的三个hash函数计算分别对应值,值分别是2、8、13,然后查询数组中这三个位置的值是否为1。
Hash1(data4)=1
Hash2(data4)=8
Hash3(data4)=12
如图:
我们可以看到data4元素的hash函数3计算之后的值是12,数组位置12的值是0,没有元素在位置12赋值过1。如果data4存在于该布隆过滤器,则一定在添加data4时会把位置12赋值1,此时位置12还是0,则说明该布隆过滤器未添加过data4元素,所以位置中有一个位置为0。则此元素一定不存在布隆过滤器中。
四、布隆过滤器误判率
刚才查询时我们发现data3没有添加过到布隆过滤器,却在布隆过滤器查询到了,这种情况就是布隆过滤器误判了。那可以不存在误判或者减少误判吗?事实上误判是一定存在的,我们可以尽可能减小误判。下面说下如何得到误判率。
4.1 参数
m:布隆过滤器的bit长度。
n:插入过滤器的元素个数。
k:哈希函数的个数。
4.2 推导过程
4.3 误判率公式
五、实现方式
5.1 Guava实现
guava是谷歌开源工具类,其中就有能直接实现布隆过滤器的方法,不需要重复造轮子。
方法名 | 功能 | 参数 | 返回值 |
put | 添加元素 | put(T object) | boolean |
mightContain | 检查元素是否存在 | mightContain(T object) | boolean |
copy | 根据此实例创建一个新的BloomFilte | copy() | BloomFilter |
approximateElementCount | 已添加到Bloom过滤器的元素的数量 | approximateElementCount() | long |
expectedFpp | 返回元素存在的错误概率 | expectedFpp() | double |
isCompatible | 确定给定的Bloom筛选器是否与此Bloom筛选器兼容 | isCompatible(BloomFilterthat) | boolean |
putAll | 通过执行的逐位OR将此Bloom过滤器与另一个Bloom过滤器组合 | putAll(BloomFilterthat) | void |
引入依赖
测试代码
5.2 Redis实现
- 开源Redisson(RBloomFilter)。
- Redis 4.0 官方提供布隆过滤器插件。
- 通过Redis提供的bitMap自己实现。
5.2.1 开源Redisson方式
Redisson方法
方法名 | 功能 | 参数 | 返回值 |
add | 添加元素 | add(T object) | boolean |
contains | 检查元素是否存在 | contains(T object) | boolean |
count | 已添加到Bloom过滤器的元素的数量 | count() | long |
getExpectedInsertions | 返回的预期插入元素的个数 | getExpectedInsertions() | long |
getFalseProbability | 返回元素存在的错误概率 | getFalseProbability() | double |
getHashIterations | 返回每个元素使用的哈希迭代次数 | getHashIterations() | int |
getSize | 返回此实例所需Redis内存的位数 | getSize() | long |
tryInit | 初始化Bloom筛选器参数 | tryInit(long expectedInsertions, double falseProbability) | boolean |
delete | 删除对象 | delete() | boolean |
引入依赖
测试代码
5.2.2 Redis 4.0 官方提供布隆过滤器插件
基础命令
命令 | 功能 | 参数 |
BF.RESERVE | 创建一个大小为capacity,错误率为error_rate的空的Bloom | BF.RESERVE {key} {error_rate} {capacity} [EXPANSION {expansion}] [NONSCALING] |
BF.ADD | 向key指定的Bloom中添加一个元素itom | BF.ADD {key} {item} |
BF.MADD | 向key指定的Bloom中添加多个元案 | BF.MADD {key} {item ...} |
BF.INSERT | 向key指定的Bloom中添加多个元素,添加时可以指定大小和错误率,且可以控制在Bloom不存在的时候是否自动创建 | BF.INSERT {key} [CAPACITY {cap}] [ERROR {error}] [EXPANSION {expansion}] [NOCREATE] [NONSCALING] ITEMS {item ...} |
BF.EXISTS | 检查一个元秦是否可能存在于key指定的Bloom中 | BF.EXISTS {key} {item} |
BF.MEXISTS | 同时检查多个元素是否可能存在于key指定的Bloom中 | BF.MEXISTS {key} {item ...} |
BF.SCANDUMP | 对Bloom进行增量持久化操作 | BF.SCANDUMP {key} {iter} |
BF.LOADCHUNK | 加载SCANDUMP持久化的Bloom数据 | BF.LOADCHUNK {key} {iter} {data} |
BF.INFO | 查询key指定的Bloom的信息 | BF.INFO {key} |
BF.DEBUG | 查看BloomFilter的内部详细信息(如每层的元素个数,错误率等) | BF.DEBUG (key} |
引入依赖
测试代码
5.2.3 通过Redis提供的bitMap自己实现
自定义方法
方法名 | 功能 | 参数 | 返回值 |
add | 添加元素 | add(String key, String element, int expireSec) | boolean |
contains | 检查元素是否存在 | contains(String key, String element) | boolean |
getExpectedInsertions | 返回的预期插入元素的个数 | getExpectedInsertions() | long |
getFalseProbability | 返回元素存在的错误概率 | getFalseProbability() | double |
getNumHashFunctions | 返回每个元素使用的哈希迭代次数 | getNumHashFunctions() | int |
getBitmapLength | 返回Bitmap长度 | getBitmapLength() | long |
BloomFilterUtils | 创建Bloom对象 | BloomFilterUtils(long expectedInsertions, double falseProbability) | BloomFilterUtils |
测试代码
六、布隆过滤器商业运用
6.1 业务场景
在C1看视频得曝光活动项目中,为了在个人中心页为拥有在架商品的用户展示活动入口,需要高效地判断用户是否有在架商品。目前存在上亿存量用户和上百万的在架商品用户。每次用户进入个人中心页时,需要查询用户是否有在架商品,以确定是否展示活动入口。然而,直接查询商品服务会导致大量的重复查询和增加服务耗时。可以使用布隆过滤器来优化此过程,它只需要几十MB内存,相比于使用Redis存储每日在架商品用户需要几GB内存,更加高效节省内存消耗。
6.2 布隆过滤器选择
实现方式 | 储存位置 | 适用场景 | 备注 |
Guava | 机器 | 单机 | 只需要机器内存不需要其他资源 |
Redisson | redis | 分布式 | 连接Redis即可使用 |
Redis插件 | redis | 分布式 | 需要对redis集群进行设置支持布隆过滤器插件 |
Redis的bitMap | redis | 分布式 | 需要自己实现添加和查询 |
对于分布式服务环境,Guava方式不适合使用,而Redis插件需要复杂的配置和高成本支持。相比之下,Redisson连接Redis并进行插入和查询的方式更适合当前场景,因此最终选择了Redisson方式。
6.3 使用布隆过滤器效果
1、内存占用情况
- 上线初期,我们将符合条件的用户存入Codis缓存中。这使得内存从1.98GB增长到9.52GB,此次数据量占用了7.54GB的内存。
- 随后,为进一步优化,我们成功将用户量缩小了25倍。这使得内存占用降至308.8MB。
- 最终,我们切换到了Redisson方式使用布隆过滤器。这次Redis内存从2.7172GB增长到2.7566GB,而数据量仅占用39.4MB的内存。
使用Codis内存占用情况
插入数据前:
插入数据后:
使用布隆过滤器内存占用情况
插入数据前:
插入数据后:
2、通过使用布隆过滤器减少对商品服务的查询,从而提升服务性能。之前需要查询商品服务来判断用户商品状态,但使用布隆过滤器后,减少了这部分服务间的调用耗时,整体流程的耗时减少了大约5ms。
作者介绍
李帅齐,转转商业后端开发工程师,目前负责商业C端相关业务系统开发(广告检索、计费以及特征工程系统等)。