布隆过滤器
Redis 布隆过滤器
应用场景
我们知道可以用HyperLogLog数据结构用来进行估数, 它非常有价值,可以解决很多精度不高的统计需求。
但是如果我们想知道某一个值是不是已经在HyperLogLog结构里面了, HyperLogLog就不能为力了, 它只提供了pfadd和pfcount方法, 没有提供pfcontains这种方法。
现在比如我们在使用新闻客户端看新闻时, 它会给我们不停的推荐新的内容, 它每次推荐都需要去重, 去掉哪些已经看过的内容, 那么问题来了, 新闻客户端推荐系统如何实现推送去重的呢?
你会想到服务器记录了用户看过的所有历史记录, 当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选, 过滤掉哪些已经存在的记录。问题是当用户量很大, 每个用户看过的新闻又很多的情况下, 这个方式, 推荐系统的去重工作在性能上跟的上么?
实际上, 如果历史记录存储在关系数据库中, 去重就需要频繁地对数据库惊醒exists查询, 当系统并发量很高时, 数据库是很难抗住压力的。
你可能又想到了缓存, 但是如此多的历史记录全部缓存起来, 那得浪费多大的存储空间, 而且这个存储空间是随着时间线性增长, 你撑得住一个月, 你能撑得住几年么? 但是不缓存的话,性能有跟不上, 这该怎么办呢?
Redis布隆过滤器(Bloom Filter)是用来专门解决这种去重问题的。 它在起到去重的同时, 在空间上还能节省90%以上, 只是稍微有点那么不精确, 也就是有一定的误判概率。
什么是布隆过滤器
布隆过滤器可以理解为一个不怎么精确的set结构, 当你使用他的contains方法判断某个对象是否存在时, 它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置合理,它的精度可以控制的相对足够精确,只会有小小的误判概率。
当布隆过滤器说某个值存在时, 这个值可能不存在;当它说不存在时, 那就肯定不存在。
在上面的使用场景中, 布隆过滤器能够准确过滤掉那些已经看过的内容, 那些没有看过的新内容, 它也会过滤掉极小一部分(误判),但是绝大多数内容它都能够准确识别。 这样就可以保证推荐给用户的内容都是无重复的。
基本使用
Redis官方提供的布隆过滤器到了Redis4.0提供了插件功能后才正式登场。过滤过滤器作为一个插件加载到Redis Server中, 给Redis提供了强大的布隆去重功能。
布隆过滤器有两个基本指令, bf.add添加元素, bf.exists查询元素是否存在, 它的用法和set集合的sadd和sismember差不多。
bf.add 只能一次添加一个元素, 如果想一次添加多个元素, 就需要用到bf.madd指令。
同样如果一次查询多个元素是否存在, 就需要用到bf.mexists指令。
127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
上面使用的是布隆过滤器知识默认参数的布隆过滤器, 它在我们第一add时候自动创建。 Redis也提供了可以自定义参数的布隆过滤器, 只需要在add之前使用bf.server指令显式创建就好了。 如果对应的key已经存在, bf.server就好报错。
bf.server有三个参数, 分别是key、error_rate(错误率)和initial_size:
- error_rate 越低, 需要的空间越大, 对于不需要过于精确的场合, 设置稍大些也没有关系, 比如说新闻推送系统, 只会让一小部分的内容被过滤掉, 整体的观看体验还是不会收到很大影响的。
- initial_size 表示语句放入的元素数量, 当实际数量超过这个值时, 误判率就会提升, 所以需要提前设置一个较大的数值避免超出导致误判率升高。
如果不是用bf.server, 默认的error_rate是0.01, 默认的initial_size 是100。
布隆过滤器的原理
每个布隆过滤器对应到Redis的数据结构里面就是一个大型的位数组和大小不一样的无偏hash函数。所谓无偏就是能够把元素的hash算的比较均匀。
想布隆过滤器中添加key时, 会使用多个hash函数对key进行hash算得一个整数索引值然后对应位数组长度进行取模运算得到一个位置, 每个hash函数都会算得一个不同位置。 再把数组的这几个位置都置为1就完成了add操作。
布隆过滤器询问key是否存在时, 和add一样, 也会把hash的几个位置都算出来,看看位数组中这几个位置是否都为1, 只要有一个位位0, 那么说明布隆过滤器的这个key不存在。 如果都是1, 这并不能说明这个key就一定存在, 只是极有可能存在, 因为这些位被置为1可能是因为其他的key存在所致。 如果这个位数组比较稀疏, 这个概率会很大,如果这个位数组比较拥挤, 这个概率就会降低。
使用时不要让实际元素远大于初始化大小, 当实际元素超过初始化大小时, 应该对布隆过滤器进行重建, 重新分配一个size更大的过滤器, 再将所有的历史元素批量add进去。因为error_rate不会因为数量超出就会急剧增加,这就给我们重建过滤器提供了较为宽松的实际。
空间占用估计
布隆过滤器的空间占用有一个简单的计算公式。布隆过滤器有两个参数, 第一个时预计元素的数量n, 第二个时错误率f。 公式根据这两个输入得到两个输出, 第一个输出是位数组的长度1, 也就是需要的存储空间大小(bit), 第二个输出是hash函数的最佳数量k。 hash函数的数量也会直接影响到错误率, 最佳的数量会有最低的错误率。
从公式中可以看出:
1、位数组相对越长(1/n),错误率f越低, 这个和直观上理解是一致的
2、位数组相对越长(1/n), hash函数需要的最佳数量也越多, 影响计算效率
3、当一个元素平均需要1个字节(8bit)的指纹空间时(1/n=8), 错误率大约是2%
4、错误率为10%,一个元素需要的平均空间为4.792个bit, 大约是5bit
5、错误率为1%, 一个元素需要的平均空间为9.585个bit, 大约是10bit
6、错误率为0.1%, 一个元素需要的平均空间为14.377个bit, 大约是15bit
实际元素超出时, 误判率会怎样变化
当实际元素超出预计元素时, 错误率会有多大变化, 它会急剧上升么, 还是平缓地上升, 这就需要另外一个公式, 引入参数t表示实际元素的倍数t,
其中k表示hash函数的最佳数量
当t增大时, 错误率f也会跟着增大, 分别选择错误率为10%, 1%, 0.1%的k值
1、错误率为10%时,倍数比为2时, 错误率就会升至接近40%, 这个就比较危险了
2、错误率为1%时, 倍数比为2时, 错误率升至15, 也挺可怕的
3、错误率为0.1%时, 倍数比为2时, 错误率升至5%,也比较悬了