持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第29天,点击查看活动详情
场景
在使用新闻客户端看新闻大时候,会给我们不停地推荐新的内容,它每次推荐要去重,去掉已经看过的内容,那么新闻客户端推荐系统如何推送去重的新闻?
方案一
服务器记录了用户看过的所有历史数据,当推荐系统推荐新闻大时候从每个用户的历史记录中进行筛选,过滤掉已经存在的记录,但是用户量很大大时候,每个用户看过的新闻很多,推荐系统的去重工作就无法跟上系统的性能了
如果记录存放在关系型数据库中,去重就需要频繁地对数据库进行 exist 查询,当并发量很高的时候,数据库很难扛住压力
但是如果存放在缓存中,那么多历史记录会浪费很大的存储空间
方案二
布隆过滤器,布隆过滤器能准确过滤掉已经看过的内容,没有看过的内容有很小的概率会被过滤掉,但绝大部分内容能精准识别
布隆过滤器
它可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,可能会误判,但是只要参数设置合理,它的精确度就足够精确,误判概率很小。这么说吧,布隆过滤器判断某值存在,它可能不存在,判断某值不存在时它一定不存在
方法
bf.add 添加元素
bf.exist 查询元素是否存在
bf.madd 一次添加多个元素
bf.mexists 一次查询多个元素是否存在
原理
在第一次 add 的时候会自动创建默认参数的布隆过滤器,需要我们在 add 之前使用 bf.reserve 指令显示创建。如果 key 已经存在,那么 bf.reserve会报错,bf.reserve有三个参数,分别是 key,error_rate 和 initial_size。要想错误率低,就需要更大的空间,如果实际放入的元素数量大于预计放入的元素数量,误判率会上升
每个布隆过滤器对应到 redis 的数据结构就是一个大型的位数组和几个不一样的无偏 hash 函数。比方说有 10 万个商品,每个商品名称是 4 个字节,那么要记录所有商品就需要 40 万个字节,而我们可以通过 bitmap 结构的布隆过滤器来实现
- 添加 key 的时候,会使用多个 hash 函数对 key 进行 hash 计算得到一个整数索引,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为1完成 add 操作
- exist 操作的时候,就是把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一位为0,说明布隆过滤器这个 key 不存在,如果都是 1 并不能说明这个 key 存在,因为有可能这个位置被设置为 1 是其他 key 导致的
这样就可以通过布隆过滤器来实现记录原本需要 40 万件商品记录
其他应用
- 爬虫系统中对 URL 进行去重,已经爬过的网页不用爬了,但是 URL 太多了,这时候就可以使用布隆过滤器,可以大幅度降低去重存储消耗
- 在 NoSQL 数据库中使用,显著降低数据库的 IO 请求数量,让用户查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询
- 邮箱系统的垃圾邮件过滤功能也使用了布隆过滤器,因为用了布隆过滤器,所以平时也会出现正常的邮箱被放到垃圾邮件目录中,这就是误判,但概率很低