Redis布隆过滤器(原理+图解)

11个月前 (04-28)
布隆过滤器(Bloom Filter)是 Redis 4.0 版本提供的新功能,它被作为插件加载到 Redis 服务器中,给 Redis 提供强大的去重功能。

相比于 Set 的去重功能而言,布隆过滤器在空间上能节省 90% 以上,但是它的不足之处是去重率大约在 99% 左右,也就是说有 1% 左右的误判率,这种误差是由布隆过滤器的自身结构决定的。俗话说“鱼与熊掌不可兼得”,如果想要节省空间,就需要牺牲 1% 的误判率,而且这种误判率,在处理海量数据时,几乎可以忽略。

应用场景

布隆过滤器是 Redis 的高级功能,虽然这种结构的去重率并不完全精确,但和其他结构一样都有特定的应用场景,比如当处理海量数据时,就可以使用布隆过滤器实现去重。

下面举两个简单的例子:

1) 示例:

百度爬虫系统每天会面临海量的 URL 数据,我们希望它每次只爬取的页面,而对于没有更新过的页面则不爬取,因策爬虫系统必须对已经抓取过的 URL 去重,否则会严重影响执行效率。但是如果使用一个 set()去装载这些 URL 地址,那么将造成资源空间的严重浪费。

2) 示例:

垃圾邮件过滤功能也采用了布隆过滤器。虽然在过滤的过程中,布隆过滤器会存在一定的误判,但比较于牺牲宝贵的性能和空间来说,这一点误判是微不足道的。

工作原理

布隆过滤器(Bloom Filter)是一个高空间利用率的概率性数据结构,由二进制向量(即位数组)和一系列随机映射函数(即哈希函数)两部分组成。

布隆过滤器使用exists()来判断某个元素是否存在于自身结构中。当布隆过滤器判定某个值存在时,其实这个值只是有可能存在;当它说某个值不存在时,那这个值肯定不存在,这个误判概率大约在 1% 左右。

1) 工作流程-添加元素

布隆过滤器主要由位数组和一系列 hash 函数构成,其中位数组的初始状态都为 0。

下面对布隆过滤器工作流程做简单描述,如下图所示:

Redis布隆过滤器

图1:布隆过滤器原理


当使用布隆过滤器添加 key 时,会使用不同的 hash 函数对 key 存储的元素值进行哈希计算,从而会得到多个哈希值。根据哈希值计算出一个整数索引值,将该索引值与位数组长度做取余运算,最终得到一个位数组位置,并将该位置的值变为 1。每个 hash 函数都会计算出一个不同的位置,然后把数组中与之对应的位置变为 1。通过上述过程就完成了元素添加(add)操作。

2) 工作流程-判定元素是否存在

当我们需要判断一个元素是否存时,其流程如下:首先对给定元素再次执行哈希计算,得到与添加元素时相同的位数组位置,判断所得位置是否都为 1,如果其中有一个为 0,那么说明元素不存在,若都为 1,则说明元素有可能存在。

3) 为什么是可能“存在”

您可能会问,为什么是有可能存在?其实原因很简单,那些被置为 1 的位置也可能是由于其他元素的操作而改变的。比如,元素1 和 元素2,这两个元素同时将一个位置变为了 1(图1所示)。在这种情况下,我们就不能判定“元素 1”一定存在,这是布隆过滤器存在误判的根本原因。

安装与使用

在 Redis 4.0 版本之后,布隆过滤器才作为插件被正式使用。布隆过滤器需要单独安装,下面介绍安装 RedisBloom 的几种方法:

1) docker安装

docker 安装布隆过滤器是最简单、快捷的一种方式:

docker pull redislabs/rebloom:latest

docker run -p 6379:6379 --name redis-rediloom redislabs/rebloom:latest

docker exec -it redis-rediloom bash

redis-cli

#测试是否安装成功

127.0.0.1:6379> bf.add www.biancheng网站站点" rel="nofollow" />

下载地址:

https://githu网站站点" rel="nofollow" />

127.0.0.1:6379> bf.add spider:url www.biancheng网站站点" rel="nofollow" />

import redis

size=10000

r = redis.Redis()

count = 0

for i in range(size):

#添加元素,key为userid,值为user0...user9999

    r.execute_command("bf.add", "userid", "user%d" % i)

#判断元素是否存在,此处切记 i+1

    res = r.execute_command("bf.exists", "userid", "user%d" % (i + 1))

    if res == 1:

        print(i)

        count += 1

#求误判率,round()中的5表示保留的小数点位数

print("size: {} ,error rate:{}%".format(size, round(count / size * 100, 5)))

执行三次测试,size 从小到大。输出结果如下:

size: 1000 , error rate: 1.0%

size: 10000 , error rate: 1.25%

size: 100000 , error rate: 1.305%

通过上述结果可以看出布隆过滤器的错误率为 1% 多点,当 size 越来越大时,布隆过滤器的错误率就会升高,那么有没有办法降低错误率呢?这就用到了布隆过滤器提供的bf.reserve方法。如果不使用该方法设置参数,那么布隆过滤器将按照默认参数进行设置,如下所示:

key #指定存储元素的键,若已经存在,则bf.reserve会报错

error_rate=0.01 #表示错误率

initial_size=100 #表示预计放入布隆过滤器中的元素数量

当放入过滤器中的元素数量超过了 initial_size 值时,错误率 error_rate 就会升高。因此就需要设置一个较大 initial_size 值,避免因数量超出导致的错误率上升。

解决错误率过高的问题

错误率越低,所需要的空间也会越大,因此就需要我们尽可能精确的估算元素数量,避免空间的浪费。我们也要根据具体的业务来确定错误率的许可范围,对于不需要太精确的业务场景,错误率稍微设置大一点也可以。

注意:如果要使用自定义的布隆过滤器需要在 add 操作之前,使用 bf.reserve 令显式地创建 key,格式如下:

client.execute_command("bf.reserve", "keyname", 0.001, 50000)

布隆过滤器相比于平时常用的的列表、散列、等数据结构,其占用空间更少、效率更高,但缺点就是返回的结果具有概率性,并不是很准确。在理论情况下,添加的元素越多,误报的可能性就越大。再者,存放于布隆过滤器中的元素不容易被删除,因为可能出现会误删其他元素情况。