目录
在介绍HyperLogLog之前,我们先思考一个常见的业务问题:如果开发一个大型网站,要记录每个网页每天的UV数据,我们应该如何实现呢?
如果统计PV那非常容易,给每个网页一个独立的Redis计数器就可以了,这个计数器的key后缀加上当天的日期。这样来一个请求,incrby一次,最终就可以统计出所有的PV数据。但是UV不一样,它要去重,也许我们可以想到一个简单的方案,为每个网页创建一个set集合来存储当天访问过此页面的用户ID。但是如果我们的页面非常巨大,比如一个爆款页面几千万的UV,需要一个很大的set集合来统计,这样就非常浪费空间了。
Redis提供了HyperLogLog数据结构用来解决这种统计问题。HyperLogLog提供了不准确的去重计数功能,虽然不准确但也不是非常不准确,标准误差为0.81%,这样的精确度已经可以满足UV统计需求了。
HyperLogLog提供了两个指令pfadd(增加计数)和pfcount(获取计数)。pfadd用法和set集合的sadd是一样的,pfcount则类似于scard。实例代码如下:
MyRedis:0>pfadd uv user1
1
MyRedis:0>pfadd uv user2
1
MyRedis:0>pfadd uv user3
1
MyRedis:0>pfcount uv
3
MyRedis:0>pfadd uv user3
0
MyRedis:0>pfcount uv
3
MyRedis:0>pfadd uv user4 user5 user5
1
MyRedis:0>pfcount uv
5
目前为止,UV统计还是非常准确的。下面我们使用脚本,插入更多的数据,看看能否继续准确:
import my_tools
client = my_tools.get_redis()
for i in range(1000):
client.pfadd('uv2', 'user%d' % i)
total = client.pfcount('uv2')
if total != i + 1:
print(total, i + 1)
break
输出结果为:
99 100
从输出结果可以看出,当我们输入第100个元素的时候,结果出现了不一致。继续增加数据到2W,看一下差距有多大:
import my_tools
client = my_tools.get_redis()
for i in range(20000):
client.pfadd('uv4', 'user%d' % i)
total = client.pfcount('uv4')
print(total, 20000)
输出结果为:
20082 20000
差了82个,误差率为0.41%,对于UV的统计需求来说,误差率还不算太高。
HyperLogLog除了pfadd和pfcount之外,还提供了pfmerge指令,用于将多个pf的计数值累加在一起形成一个新的pf值。比如我们的网站中有多个页面,需要将这几个页面的数据进行合并,其中页面的UV访问量也需要合并,那这个时候就需要使用pfmerge命令了。
HyperLogLog在计数比较小时,它的存储空间是采用稀疏矩阵空间占用较小,当计数慢慢变大的时候,稀疏矩阵占用的空间逐渐超过了阈值时会一次性转变为稠密矩阵,占用空间大概是12K。由于HyperLogLog占用的空间相对比较大,所以不适合统计单个用户相关的数据。