1.场景:在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。新闻客户端推荐系统如何实现推送去重?
想法:
(1)在服务器中记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?
如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行exists查询,当系统并发量很高时,数据库是很难扛住压力的。
使用缓存?如此多的历史记录全部缓存起来,那得浪费多大存储空间。而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年吗?
应用:布隆过滤器。专门用来解决这种去重问题。在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。
2.布隆过滤器是什么
简单理解为一个不怎么精确的set 结构。使用它的contains方法判断某个对象是否存在,可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。
这里需要注意:布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。(布隆过滤器对于已经见过的元素肯定不会误判,它只会误判那些没见过的元素)
套在上面的使用场景中,布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过的新内容,它也会过滤掉极小一部分 (误判),但是绝大多数新内容它都能准确识别。这样就可以完全保证推荐给用户的内容都是无重复的
3.redis的布隆过滤器
布隆过滤器到了Redis 4.0提供了插件功能之后才正式登场
4.基本使用
(1)bf.add添加元素,只能一次添加一个元素;bf.madd一次添加多个元素;bf.exists查询元素是否存在;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.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) 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
(2)自定义参数的布隆过滤器
需要我们在add 之前使用bf.reserve指令显式创建。如果对应的key 已经存在,bf.reserve会报错。bf.reserve有三个参数,分别是key, error_rate和initial_size。错误率越低,需要的空间越大。initial_size参数表示预计放入的元素数量,当实际数量超出这个数值时,误判率会上升。
需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用bf.reserve,默认的error_rate是0.01,默认的 initial_size是100。
5.注意事项
initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多
6.布隆过滤器原理
布隆过滤器对应到Redis的数据结构里面就是一个大型的位数组和几个不一样的无偏hash函数。所谓无偏就是能够把元素的has值算得比较均匀。
向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个hash函数都会算得一个不同的位置。再把位数组的这几个位置都置为1就完成了add操作。
向布隆过滤器询问 key 是否存在时,跟add一样,也会把hash的几个位置都算出来,看看位数组中这几个位置是否都位1,只要有一个位为0,那么说明布隆过滤器中这个key 不存在。如果都是1,这并不能说明这个key就一定存在,只是极有可能存在,因为这 些位被置为1可能是因为其它的key 存在所致(为什么说存在可能不存在,说不存在就一定不存在)
使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量add进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为error_rate不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间
以上为学习《Redis深度历险核心原理和应用实践》笔记