【Redis基础和应用】(四)HyperLogLog & BloomFilter

HyperLogLog

0. 前言

对于一个常见的业务问题:如果你负责开发维护一个大型的网站,有一天需要实现一个需求,统计每个页面每天的UV数据,那么将如何实现?

这里补充一下UV和PV的知识

PV(访问量):即Page View, 即页面浏览量或点击量,用户每次刷新即被计算一次。是总的用户访问量。

UV(独立访客):即Unique Visitor,访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次,此时,需要对用户进行去重,然后统计出每天有多少个用户访问量

总结:UV是PV去重后的结果。


如果是统计PV,那就很简单,给每个页面分配一个Redis的计数器,把这个计数器的Key后缀加上当天的日期,这样每一次来一个请求,执行incrby一次,最终就可以统计每个页面的访问量数据。

但是UV需要去重,同一个用户多次访问一个页面只能计数一次,这就要求每一个网页请求都需要带上用户的ID, 无论是登录用户还是未登录用户。都需要一个唯一ID来标识。

最容易想到的方案是:使用Reis的set,为每一个页面设置set来存储所有当天访问过此页面的用户ID,当有新的请求时,使用zadd将用户ID放入set中,使用scard来统计集合的大小。这个大小就是页面的UV数据。

但是,当页面访问量比较大的时候,使用set来统计非常浪费空间。其实,实际应用中,我们不需要太精确的统计UV数据,仅仅是一个不精确的大概数据就可以。

HyperLogLog就是为了解决这种统计问题而设计的。它提供不精确的去重计数方案,标准误差在0.81%左右。这样的精确度已经可以满足上面的UV统计需求。
在这里插入图片描述

1. 使用方法

HyperLogLog提供了三个指令pfaddpfcountpfmerge, 一个是增加计数,一个是获取计数,最后一个是合并计数。

>pfadd leesure user1 # 注意,这里的key长度不能设置的太短
(integer) 1
> pfadd leesure user2
(integer) 1
> pfcount leesure 
(integer) 2

当加入100个元素时, 误差在0.01%, 当插入10万条数据时,误差在0.277%。对于UV统计需求来说,误差率也不算高,如果将相同的数据重新加一遍,输出的结果一样,说明HyperLogLog确实具备去重功能。

  • pfmerge指令,用于将多个pf计数累加在一起形成一个新的pf。比如,将两个页面的UV值进行合并。

需要注意的是,HyperLogLog这个数据结构需要占据12KB的存储空间,所以不适合统计单个用户相关的数据。但是Redis对HyperLogLog的存储进行了优化,在计数比较小时,存储空间采用稀疏矩阵存储,空间占用很小。计数比较大时,存储空间采用稠密矩阵,此时才会占用12KB的存储空间。

BloomFilter

0. 前言

HyperLogLog数据结构可以用来估数,可以解决很多精确度要求不高的统计问题,但是如果想知道一个数据是不是已经在HyperLogLog结构里面,那么就无法判断了。

比如在推荐系统中,系统每次推荐的内容都是我们没有看过的新内容,因此要去掉那些已经看过的内容。但是如果使用HyperLogLog就不能判断当前的内容是新的数据,而且HyperLogLog不适合针对单个用户使用。

BloomFilter就是专门用来过滤掉已经存在的记录,解决去重问题。同时能在空间上能节省90%,只是稍微有一点不精确,有一定的误判概率。

1. 布隆过滤器

可以把BloomFilter理解为不怎么精确的set结构,当判断某个数据是否存在时,布隆过滤器可能会误判。只要参数设置在合理范围内,它的精确度也可以控制在相对足够精确范围内,只有小小的误判概率。

当布隆过滤器说某个值存在时,这个值可能不存在;当说某个值不存在时,这个值一定不存在。

布隆过滤器能准确过滤掉那些用户已经看过的内容,那些用户没有看过的新内容,它也可能过滤掉一小部分(误判),但是绝大多数新内容都能准确识别。这样就可以保证推荐给用户的内容都是无重复的。
在这里插入图片描述

2. 使用方法

Redis4.0以后,Redis官方提供了BloomFilter的插件,它作为一个插件加载到Redis Server中,给Redis提供了去重功能。因此如果想使用此方法,就需要安装插件。

这里使用docker安装

> docker 

BloomFilter有两个基本指令bf.add(查看元素)和bf.exists(查询元素是否存在)。注意bf.add一次只能添加一个元素,如果想要添加多个,就需要用到bf.madd指令。一次查询多个元素需要用到bf.mexists指令。

>bf.add leesure user1
(integer) 1
> bf.add leesure user2
(integer) 1
> bf.madd leesure user3 user4
(integer) 1
(integer) 1
> bf.mexists leesure user1 user2 user5
(integer) 1
(integer) 1
(integer) 0

注意,Java客户端Jedis-2.x没有提供指令扩展机制,所以无法直接使用Jedis来访问Redis Module提供的bf.xxx指令。因此RedisLabs提供了一个单独的包JReBloom,但是它是基于Jedis3.0,但是Jedis3.0这个包目前(2018-09)还没有进入release阶段,没有进入maven仓库。需要去github上下载源码。如果不想这么麻烦,可以使用lettuce,它是Java的另一个Redis客户端,相比于Jedis,它很早就支持了指令扩展。

🤗BloomFilter对于已经出见过的元素肯定不会误判。它的误判只会出现在没有见过的元素中。

上面使用的BloomFilter只是默认参数的布隆过滤器,它会在第一次add的时候自动创建。Redis还提供了自定义参数的布隆过滤器。需要使用bf.reserve指令显示创建。如果对应的key已经存在,创建会报错。

bf.reserve有三个参数:container_nam(容器名), error_rate(容错率,默认为0.01), 和initial_size(默认为100)

注意,error_rate越低,需要的空间越大。initial size表示预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,所以需要提前设置一个交达的数量避免超出导致误判率升高。

🎯注意事项:

  1. 布隆过滤器的initial size设置的太大,会浪费存储空间。设置的过小,会影响准确率,用户在使用之前一ing要尽可能地估计元素数量,还需要加上一定的冗余空间以避免实际元素可能会高出预估大小。
  2. error_rate越小,需要的存储空间就越大,对于不需要过于精确的应用场景,error_rate设置的稍微大一些也没关系。比如在新闻客户端的去重应用上,误判率高一些只会让小部分文章不会被看到,文章的整体阅读量不会因为这一点误判率就带来巨大的变化。

3. 原理⭐️

布隆过滤器的数据结构就是一个大型的位数组和几个不一样的无偏hash函数。如图的f,g,h就是无偏hash函数。所谓无偏就是能够把元素的hash值计算得比较均匀,让元素被hash映射到的位数组中的位置比较随机。

在这里插入图片描述

  1. add过程: 向布隆过滤器中添加元素时,会使用多个hash函数对key进行hash运算,得到的索引值对数组长度进行取模运算得到一个位置。每一个hash函数都会算得一个不同的位置,然后把数组的这几个位置都设置为1,这就完成了add操作。

  2. exists过程:向布隆过滤器查询一个key是否存在时,也会把hash的几个位置都算出来,看看位数组中的这几个位置是不是都为1,只要有一个位为0,就说明过滤器中不存在这个key。但是如果这几个位置都是1,也不能说明这个key就一定存在,只是极有可能存在。因为这些位置被置为1 可能是因为其他的key存在导致的。

  3. 如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个数位比较稠密,判断正确的概率就会降低。

注意,使用时,不要让实际元素数量远大于初始数量,当实际元素数量开始超过初始数量时,应该对布隆过滤器进行重建,重新分配一个size更大的过滤器,再将所有的历史元素批量add进去。

空间占用估计

布隆过滤器的空间占用计算有一个简单的计算公式,但是推导起来比较麻烦。这里直接给出。n表示预计元素数量。k表示最佳的hash函数数量,f表示错误率。
k = 0.7 ∗ 1 n f = 0.618 5 1 n k = 0.7*\frac{1}{n} \\f = 0.6185^{\frac{1}{n}} k=0.7n1f=0.6185n1
从公式可以看出:

  1. 位数组越长,错误率f越低,需要的最佳hash函数的个数也越多,因此也一定程度影响计算效率。
  2. 当一个元素平均需要一个字节(Byte)的指纹空间(1/n)=8,错误率大约为2%。
  3. 当错误率为10%时,一个元素需要的平均指纹空间为4.792bit;
  4. 当错误率为1%时,一个元素需要的平均指纹为9.585bit;
  5. 当错误率为0.1%时,一个元素需要的平均空间指纹为14.377bit。

可能会想,当错误率为0.1%时,一个元素需要占据15bit,相对于set集合的空间优势就不是那么明显了,但是需要明确的是:set会存储每个元素的内容,布隆过滤器仅仅存储元素的指纹。

set存储的内容就是字符串的长度,它一般会有多个字节,甚至是几十个或者上百个字节,每一个元素本身还需要一个指针被set集合引用,这个指针又会占去4Byte或者8Byte。指纹空间所需不足2个字节,所以布隆过滤器的空间优势还是非常明显的。

在线布隆过滤器空间计算.

实际元素超出时,误判率会发生怎么样的变化

当实际元素超出预计元素时,错误率会急剧上升还是会缓慢上升?这里就需要另一个公式说明,其中:t表示实际元素和预计元素的倍数。即实际元素是预计元素的t倍。k表示最佳hash数量。f表示错误率。
f = ( 1 − 0. 5 t ) k f = (1-0.5^t)^k f=(10.5t)k
当t增大时,错误率f也会增大。

  1. 当错误率为10%,t=2时,错误率会上升指接近40%
  2. 当错误率为1%, t=2时,错误率会上升指接近15%
  3. 当错误率为0.1%,t=2时,错误率会上升指接近5%

Redis4.0之前的版本使用布隆过滤器的备选方案

Redis4.0版本布隆过滤器的可替代方案有:

  1. Python版本库:pyreBloom
  2. Java版本库:orestes-bloomfilter

4. 其他应用

  1. 在爬虫系统中,我们需要对URL进行去重,对于已经爬过的网页就就不需要再爬了。如果URL太多,使用一个集合去装下这些URL地址,是非常浪费空间的,此时可以使用布隆过滤器,大幅度降低存储消耗,只不过也会使得爬虫系统错过少量的未访问的接面。
  2. 布隆过滤器在NoSQL数据库中使用得非常广泛,如HBase, Cassandra,LevelDB和RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库得IO请求数量,当用户来查询某个row时,可以通过布隆过滤器过滤掉大量不存在得row请求,然后再去数据库中查询
  3. 邮件系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用到了这个过滤器,所以平时也会有某些正常的邮件被放入到了垃圾邮件目录中,这就是误判导致的,但是这种误判的概率很低。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅宝儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值