布隆过滤器
0x00 适用场景
一开始学这个布隆过滤器是为了一个分布式爬虫的项目,在这个项目中有一个需求就是判断队列中的URL是否先前已经爬取过了。如果先前已经爬取过了,那就不爬了,如果没有,那就爬取这个URL。然后就了解到了这个布隆过滤器和RedisBloom
。
布隆过滤器说白了主要应用于在大量数据中判断给定数据是否存在。 这是一种基于概率的算法,什么意思呢,就是说,不存在的一定返回false
,而返回true
的却不一定存在。也就说它有一定的误判率。
0x01 基本原理
布隆过滤器的基本组件其实很简单,一系列的hash函数以及一个存放位序列的大表。现在我们假定一个长度为8的表,初始状态下,表中的元素均为0:
1 | +---+---+---+---+---+---+---+---+ |
现在有一组Hash函数,hash1(data)
和hash2(data)
。对于一个给定数据liu
,我们假设hash1
和hash2
函数的返回值分别为为2, 4,那在这个大表中,翻置下标为2和4的元素为1,如下:
1 | +---+---+---+---+---+---+---+---+ |
对于另外一个给定数据shao
,我们假设hash1
和hash2
函数的返回值分别为为3, 2,继续翻置,如下:
1 | +---+---+---+---+---+---+---+---+ |
当我们试图判断数据liu
是否存在时,我们对这组新的数据计算那两个Hash函数,如果返回的下标在大表中所指向的字节均为1的话,那么数据存在,否则,数据不存在。
但是这样就会出现一个问题,在上面的例子中,输入数据liu
和shao
均翻置了下标为2的元素,这一位被2个不同的值的hash共同覆盖了。假设我们要删除数据liu
然后把下标为2和4的数据重新置为0的话,那么当我们试图判断数据shao
是否存在时就会返回false
,显然这是错误的,因为我们并没有删除数据shao
。基于此点布隆过滤器是不支持删除的。
除此之外,对于两组不相同的数据,两个hash函数可能返回相同的下标地址,假设对于输入数据liu
和qun
而言,两组hash函数均分别返回下标2和4,那我当插入数据liu
之后,在过滤器中判断数据qun
是否存在,过滤器是会返回true
的,这就是误判。在Guava中,BloomFilter
默认的误判率是3%,这个值在Guava中可以调的,误判率越低,这个大表就越大。这个大表的大小m
和误判率fpp
以及预估数据量n
之间的关系为:
hash函数的个数k
与表的大小m
和预估数据量n
之间的关系为:
有此我们可见,误判率的大小和大表的大小m
和hash函数的个数k
都是呈负相关的。同时还有一幅图可以参考一下:
在Guava中,计算大表长度m
和hash函数的个数k
就是用的如上公式:
1 | /** |
0x02 RedisBloom
的使用
0x00 基于docker的快速部署
我们可以使用docker快速部署一个RedisBloom
应用:
1 | docker pull redislabs/rebloom:latest # 拉取镜像 |
0x01 控制台下使用RedisBloom
首先我们进入刚刚用docker启动的RedisBloom
应用:
1 | docker exec -it redis-redisbloom redis-cli |
BF.RESERVE
:创建一个新的布隆过滤器
1 | BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] [NONSCALING] |
必选参数:
key
:新过滤器的名字error_rate
:预设的错误率capacity
:预计添加到布隆过滤器中的元素的个数
可选参数:
NONSCALING
:如果设置了此项,当添加到布隆过滤器中的数据达到初始容量之后,不会扩容过滤器,并且超过初始设置容量的添加会抛出异常。expansion
:与上一项相反,当设置了此项,布隆过滤器中存储的元素达到初始容量后会自动创建一个子过滤器,子过滤器的大小是上一个过滤器大小乘以参数expansion
。
复杂度为:。
示例:
1 | 127.0.0.1:6379> BF.RESERVE newFilter 0.03 100 EXPANSION 2 |
创建一个新的布隆过滤器命名为
newFilter
,预设的错误率为0.03 (3%),预计添加100个元素,超出之后自动创建子过滤器,子过滤器的大小是最后一个过滤器大小的2倍。
BF.ADD
:向布隆过滤器中添加一个元素,BF.MADD
:批量添加元素
1 | BF.ADD {key} {item} |
key
:过滤器名item
:需添加的元素
返回结果为1表示插入成功,返回结果为0表示插入的元素此前可能已经存在,也可能是发生了冲突,也就是此前添加的元素和当前添加的元素产生了相同的hash值。对于BF.MADD
将会返回一个数组,数组内各数意义上同。
复杂度:,为插入的元素的个数,为hash函数的个数。
示例:
1 | 127.0.0.1:6379> BF.ADD newFilter shaoqun-liu |
BF.EXISTS
:判断值是否在布隆过滤器中存在,BF.MEXISTS
:批量判断
1 | BF.EXISTS {key} {item} |
key
:过滤器名item
:查询的元素
返回0表示元素不存在,返回1表示元素可能存在。
复杂度:,为插入的元素的个数,为hash函数的个数。
示例:
1 | 127.0.0.1:6379> BF.EXISTS newFilter shaoqun-liu |
BF.INFO
:查询某布隆过滤器的详细信息
1 | BF.INFO {key} |
key
:过滤器名
示例:
1 | 127.0.0.1:6379> BF.INFO newFilter |
其它命令可参考官方文档:
RedisBloom Bloom Filter Command Documentation
0x02 Java API
首先,我们可以使用Maven导入依赖:
1 | <!-- https://mvnrepository.com/artifact/com.redislabs/jrebloom --> |
我在我们的项目中,将其Client
注册成了一个bean,方便日后调用,首先我们在项目的application.yml
文件中配置了相关的Redis地址信息:
1 | redis: |
这几个字段都是我自定义的,如果有需要可以自己设置,然后我们创建一个Spring boot的Configuration类,导出用于操作的Client
的bean,如下:
1 | package com.cstiaoji.spider.configuration; |
然后在我们的业务逻辑代码中,就可以使用如下代码进行相关的操作了:
1 | // 向名为cstiaoji-spider的布隆过滤器中添加值"www.baidu.com" |
更多信息可参考当前项目GitHub。