Redis提供了hyperloglog算法的实现,为了通俗易懂讲人话,我先省去大量原理讲过程,再提原理中几个重要的点,当然有更多精力和能力的还是直接看数学推导比看csdn强。

1、hll目标

首先,hyperloglog的场景来源是:

当你用hashmap或set进行基数统计,计算当日来访问网站的UV时,会产生一个问题就是集合的大小随着访问人数的增多,线性增长,到访问量很多的时候就已经存在问题了。

一般而言,优化一个算法的方式大概率就是(1)完全换一种方式(2)近似计算,hyperloglog就是一种近似计算,用一定的错误率,优化大量增加元素带来的空间占用。

他的数学来源就是概率论中的伯努利试验,连续抛三次硬币,第一次出现正面的概率是多少?那你应该能机制的答出来,第一次反面1/2,第二次反面1/2,第三次正面1/2,抛三次第一次出现正面的概率概率1/8。那么反过来,至少抛多少轮次(注意不是次数,是多少轮次至少一次抛硬币结果001)硬币,才能在第三次抛硬币时,第一次出现正面,那你大概也能说出来,至少8次。

2、为了达到目标,数学理论的支持

hyperloglog估算数据量的做法和这个类似,你可以感受到,hll就是用概率的方法估算样本的容量,那么一定是有误差的,而且也一定为了去减小这个误差提供优化的方式,比如欧皇第一轮次直接001,非酋就是100次了,也抛不出001,抛的都是些010,100,110之类的怪东西,显然这个时候,算数平均数能做的十分有限,核心就是算数平均数不能防止突变值的影响,而且会很大,比如非酋和欧皇平均值50,实际上概率上只有8。

也因此引出了hll的第二个关键点,调和平均数。调和平均数的计算公式:

redis hlog redis hloglog_redis hlog

看一眼你就懂,比如刚刚的非酋和欧皇的调和平均数H = 2/(1/1+1/100)=2,这明显比算数平均数考虑,因为你能感觉到,他是向某个值收敛的(比如做了100次,每次都恰好8,那就是完美的8),而不是发散的(省略数学证明)。那我们取更合理的值试一试:

比如序列为了达成001序列,各个挑战者们达成了7,8,9,8,12,15,和突变值50,

理论正确值:8

利用算数平均数计算得到结果:15.57

利用调和平均数计算得到:10.39

当然,为了提升hll的正确率,扣穴家还想到了用分桶的思路,计算每个桶预计结果的加权平均值,进一步提升准确率,没有复杂计算,时间复杂度上没什么负担,总之最后的最后,得到了计算估计数量的公式:const和分桶的数量m有关,p=log2m

redis hlog redis hloglog_perl_02

3、 Redis的实现

redis实现就提供了三个函数,实现一个类似set的功能,毕竟从使用层面来说,我就是希望它是一个set,虽然hll只是一种算法。

一个pfadd,提供添加元素功能(类似set的add),一个pfcount,获取我当前预估的uv(类似set的size()),一个pfmerge合并两页面的uv(类似addAll)。

> pfadd user mango
(integer) 1
> pfadd user zhangsan
(integer) 1
> pfadd user lisi
(integer) 1
> pfadd user mango     #重复则不计数
(integer) 0
> pfcount user
(integer) 3

> pfadd paper mango
(integer) 1
> pfadd paper zhangsan
(integer) 1
> pfmerge pv user paper    #合并
OK
> pfcount pv
(integer) 3

小结:

在空间上,一个桶6bit(6bit所能表示的最大的数就是111111也就是64,也就是说我能估计到2^64次方,18446744073709551616,这个数几乎可以说是目前地球人口访问同一个网站都难以达到的),16348个桶就是12KB,所以redis利用12KB,预估了一个最大2^64的数,错误率1%,这就是hyperloglog的终极目的。

同时,利用set求并这种操作,也能让本来穿行的数据,并行化,同时的同时,redis还利用稀疏存储结构优化了12KB的空间,在0很多的情况下优化了存储空间,甚至不用达到12KB

以上

巨人的肩膀:

走近源码:神奇的HyperLogLog - 知乎