目录
前面我们使用HyperLogLog数据结构来进行估数,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是否在HyperLogLog里面,它就无能为力了。因为HyperLogLog只提供了pfadd和pfcount方法,没有提供pfcotains这种方法。
比如我们在使用新闻客户端看新闻的时候,它会给我们不停的推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重呢?也许我们会想到,我们可以将每个用户的历史记录全部存起来,当用户访问的时候过滤掉那些已经存在的记录。问题是如果用户量很大,每个用户看的内容又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上吗?而且,随着时间的增长,所有的历史记录保存下来,得需要很大的存储空间。
布隆过滤器就是专门用来解决这种去重问题的,它在起到去重的同时,在空间上还能节省90%以上,只是稍微有那么点不准确,也就是说有一定的误判率。
1.什么是布隆过滤器
布隆过滤器可以理解为一个不怎么精确的set结构,当我们使用contains方法判断某个对象是否存在时,它可能会发生误判。只要我们设置合理的参数,它的精确度可以控制的相对足够精确,只会有很小的误判概率。当布隆过滤器说某个值存在时候,这个是可能不存在;当它说不存在的时候,那就肯定不存在。在上面的场景中,布隆过滤器能够准确过滤掉那些已经看过的内容,对于没有看过的新内容,它也会过滤掉很小的一部分(误判),但是对于绝大部分的新内容它都能准确识别。这样就可以保证推荐给用户的内容都是没有重复的。
2.Redis的布隆过滤器
Redis官方提供的布隆过滤器到了Redis4.0提供了插件功能之后才正式登场。下面我们来体验一下Redis的布隆过滤器。
2.1 安装步骤
具体安装步骤如下:
git clone https://github.com/RedisBloom/RedisBloom.git
cd redisbloom
make
将redisbloom.so拷贝到/path/to目录,并在redis.conf文件中添加如下代码:
loadmodule /path/to/redisbloom.so
将redis进行重启,重启完成后即可使用。
2.2 布隆过滤器基本使用
布隆过滤器有两个基本指令,bf.add添加元素,bf.exists查询元素是否存在。注意bf.add一次只能添加一个元素,如果想要一次添加多个,就需要使用bf.madd指令;同样如果需要一次查询多个元素是否存在,就需要用到bf.mexists指令。
MyRedis:0>bf.add martin-bloom user1
1
MyRedis:0>bf.add martin-bloom user2
1
MyRedis:0>bf.add martin-bloom user3
1
MyRedis:0>bf.exists martin-bloom user1
1
MyRedis:0>bf.exists martin-bloom user2
1
MyRedis:0>bf.exists martin-bloom user3
1
MyRedis:0>bf.exists martin-bloom user4
0
MyRedis:0>bf.madd martin-bloom user4 user5 user6
1) 1
2) 1
3) 1
MyRedis:0>bf.mexists martin-bloom user4 user5 user6 user7
1) 1
2) 1
3) 1
4) 0
目前来看还是非常精确的,没有出现误判。下面我们用Python脚本加入更多的元素,看看有什么效果:
import my_tools as tools
client = tools.get_redis()
for i in range(100000):
client.execute_command('bf.add', 'martin-view', 'page%d' % i)
ret = client.execute_command('bf.exists', 'martin-view', 'page%d' % i)
# 出现误判
if ret == 0:
print('出现误判%d' % i)
break
执行上面的代码后我们发现没有出现误判,那是因为布隆过滤器对于已经见过的元素永远都不会误判,它只会误判那些没有见过的元素,我们修改一下上面的脚本,使用bf.exists去查找没有见过的元素,看看它是不是以为自己见过了。
client = tools.get_redis()
client.delete('martin-view')
for i in range(100000):
client.execute_command('bf.add', 'martin-view', 'page%d' % i)
ret = client.execute_command('bf.exists', 'martin-view', 'page%d' % (i + 1))
# 出现误判
if ret == 1:
print('出现误判%d' % i)
break
运行以后,我们发现在103的时候出现误判。我们计算一下误判率:
import my_tools as tools
client = tools.get_redis()
client.delete('martin-view')
error_count = 0
for i in range(10000):
client.execute_command('bf.add', 'martin-view', 'page%d' % i)
ret = client.execute_command('bf.exists', 'martin-view', 'page%d' % (i + 1))
# 出现误判
if ret == 1:
print('出现误判%d' % i)
error_count += 1
print('误判率%.4f' % (error_count / 10000))
从输出结果可以看出,误判率大约为1.38%,误判率有点高,那应该如何降低误判率呢?Redis提供了自定义参数的布隆过滤器,需要我们在add之前使用bf.reserve指令显式创建。bf.reserve有三个参数,分别是key,error_rate和initial_size。initial_size参数表示预计放入的元素数量,当实际数量超出这个数值时,误判率就会上升。所以需要提前设置一个较大的数值避免超出导致误判率升高,如果不使用bf.reserve,默认的error_rate是0.01,默认的initial_size为100。
import my_tools as tools
client = tools.get_redis()
client.delete('martin-view')
error_count = 0
client.execute_command('bf.reserve', 'martin-view', 0.01, 11000)
for i in range(10000):
client.execute_command('bf.add', 'martin-view', 'page%d' % i)
ret = client.execute_command('bf.exists', 'martin-view', 'page%d' % (i + 1))
# 出现误判
if ret == 1:
print('出现误判%d' % i)
error_count += 1
print('误判率%.4f' % (error_count / 10000))
误判率为0.04%
2.3 使用注意事项
布隆过滤器的initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。
实际中,布隆过滤器的error_rate设置的越小,需要的存储空间就会越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。
3.布隆过滤器的原理
学会了布隆过滤器的使用,下面我们聊一下实现原理:
每个布隆过滤器对应到Redis的数据结构里面就是一个大型的位数组和几个不一样的无偏hash函数。所谓无偏就是能够把元素的hash值算的比较均匀。
向布隆过滤器中添加key时,会使用多个hash函数对key进行hash计算,计算出一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个hash函数都会算得一个不同的位置。再把位数组的这几个位置都置为1就完成了add操作。
向布隆过滤器询问key是否存在时,跟add一样,也会把hash的几个位置都算出来,看看位数组中的这几个位置是否都为1,只要有一个位置是0,那么说明布隆过滤器中这个key不存在。如果都为1,这并不能说明这个key就一定存在,只是极有可能存在,因为这些位置都置为1可能是因为其他的key存在导致。如果这个位数组比较拥挤,这个概率就会很大,如果这个位数组比较稀疏,这个概率就会降低。
使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个size更大的过滤器,再将所有的历史元素批量add进去(这就要求我们在其他的存储器中记录所有的历史元素)。因为error_rate不会因为数量超出就急剧增加,这就会给我们重建过滤器提供了较为宽松的时间。
3.1 空间占用估算
布隆过滤器的空间占用有一个简单的计算公式,但是推导过程繁琐,这里直接省去。公式如下:
k=0.7*(l/n)
f=0.6185^(l/n) // ^表示次方计算
布隆过滤器在使用之前,根据我们的设计场景,一般有两个已知参数:第一个是预计元素的数量n;第二个是错误率f。公式根据这两个输入得到两个输出,第一个输出是位数组的长度l,也就是需要的存储空间大小(bit),第二个输出是hash函数的最佳数量k。hash函数的数量也会直接影响到错误率,最佳的数量会有更低的错误率。从公式中我们可以得出:
- 位数组相对越长(l/n),错误率f越低
- 位数组相对越长(l/n),hash函数需要的最佳数量也越多,影响计算效率。
3.2 布隆过滤器的其他功能
布隆过滤器在NoSQL数据库领域使用非常广泛,我们平时使用的HBase、Cassandra、LevelDB、RocksDB内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的IO请求数量。当用户来查询某个row时,可以先通过内存中的布隆过滤器过滤掉大量不存在的row请求,然后再去磁盘进行查询。
邮件系统的垃圾邮件过滤功能也普遍使用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某个正常的邮件被放到了垃圾邮件目录中,这个就是误判所致,概率极低。