Redis
HyperLogLog
是用来做基数统计的算法,HyperLogLog
在优点是,在输入的元素的数量或者体积非常大时,计算基数占用的空间总是固定的、并且非常小。在Redis
里每个HyperLogLog
键只需花费12KB内存,就可以计算接近 264 个元素的基数。因为HyperLogLog
只是计算输入元素的基数,而不会储存元素的本身,因此HyperLogLog
不会返回输入的元素。
1、什么是基数
比如数据集{1,3,5,7,9,3,5,7},那么这个数据集的基数集为{1,3,5,7,9},基数为5(不重复的元素)。基数估计就是在误差可接受的范围内,快速计算基数。
2、常用命令
- pfadd key element[element...]:将数据添加到HyperLogLog数据结构中。
- pfcount key [key...]:返回给定
HyperLogLog
的基数估计值。 - pfmerge destKey sourceKey[sourceKey]:将多个
HyperLogLog
合并为一个HyperLogLog
。
3、HyperLogLog特点
1、占用内存小,12KB的内存大小,可以统计将近264 个元素。
2、计数存在一定的误差,但整体误差率较低。标准误差为0.81%。
*在Java中,我们知道long类型占用8字节,1byte=8位,即long类型能表示的最大数值为263 -1。对应上面的 264个数。假设现在有264个数。那么按照long,以及1KB=1024字节。那么264个数占用的内存为(264 8)/1024。其占用的内存远远高于12KB。
4、HyperLogLog原理
4.1、伯努利实验
在了解为什么HyperLogLog能在占用内存那么小的情况下,来统计那么大的数据。需要先了解下伯努利实验。
伯努利试验是数学中概率论的一部分内容。它的典故来源于抛硬币。
硬币只有正反两面(正好对应计算机中的0和1),最终出现正反面的概率都是50%。假设,我们一直抛硬币,只要出现一次正面,就记为一次完整实验。中间可能一次就出现正面,也可能10次才出现正面。不论抛了多少次,只要出现一次正面,就记为一次完整的实验。这个实验就是伯努利实验。
那么对于n次伯努利实验。意味着出现了 **n**
次正面。每一次的结果都有可能不同。假设第一次伯努利实验抛掷次数为 **k1**
,以此类推,第 **n**
此为 **kn**
。
其中,在 n
次实验中,必然会存在一次是抛掷次数最大的。假设有一次伯努利实验抛掷了12次才出现正面,是最多的一次,我们将这个数记为 **k_max**
。
在伯努利实验中,很容易得出以下结论:
1、n次伯努利实验中的抛掷次数都不大于k_max。
2、n次伯努利实验过程中最少有一次抛掷次数等于k_max。
其中n和k_max有一个估算关联关系 **n=2^k_max**
(我也不知道怎么推导的)。
假如有一轮伯努利实验的例子如下:
**第一次试验: 抛了3次才出现正面,此时 k=3,n=1 **
**第二次试验: 抛了2次才出现正面,此时 k=2,n=2 **
**第三次试验: 抛了6次才出现正面,此时 k=6,n=3 **
第n 次试验:抛了12次才出现正面,此时我们估算, n = 2^12
假设在上面的例子中,我们总共实验了3组,那么最大实验次数k_max就是6,那么由公式 **n=2^k_max**
可得n=2^6!=3。由此可知,在实验次数很少时,这种估算结果误差很大。
4.2、估算的优化
在上面的3组例子中,我们称为一轮实验。如果只进行一轮的话,当n足够大时,误差也会变小,但不是足够小。
因此是否可以进行多轮实验,然后取每轮实验的 **k_max**
平均值。例如进行100轮实验,然后再取平均数k_max/100。最终在估算出n。下面是 **LogLog**
的估算公式:
其中,DVLL 对应的就是n;m对应实验的轮次;头上有一横的R就是平均数(k_max1+k_max2+...+k_maxm)/m;constant是修正因子,可以通过修改这个数值还改变误差率。
通过这种增加实验轮次,然后取平均数是LogLog的做法。而HyperLogLog采用的不是平均数而是调和平均数。调和平均数和算术平均数的最大的区别在于调和平均数不容易受大的值影响。
求平均工资:
A的是1000/月,B的30000/月。采用平均数的方式就是: (1000 + 30000) / 2 = 15500
采用调和平均数的方式就是: 2/(1/1000 + 1/30000) ≈ 1935.484
**调和平均数公式为,**∑ 是累加符号:
HypeLogLog结合调和平均数的计算公式为:
4.3、HyperLogLog的做法
4.3.1、bit串
在Redis的HyperLogLog中,其首先将要保存的元素通过hash函数计算得到其64位的hash值。并将这个hash值转换成bit串。例如hash值是5,那么bit串就是"101"。
那么为什么要转换成bit串呢?是为了和伯努利实验的正反面对应上。bit串中的0和1就对应了伯努利实验的正面和反面。假如一个数据的hash值的bit串是"100010000",那么从低往高,从右往左数,第一个1的位置就是出现正面的次数。
那么基于上面的估算结论:我们就可以根据最大的抛掷次数来估算出大概进行了多少次实现。同样的,也就可以根据存入的数据,转化后出现1的位置k_max来估算出,大致存了多少数据。
4.3.2、分桶
HyperLogLog中的分桶其实就是应伯努利实现估算优化中的多轮。每一个桶对应一轮的伯努利实验。转换成计算机存储就是:存储的是一个单位是bit,长度为L的大数组S,将S分为m组,其中m就是多轮实验。每组占用的bit个数是平均记为P,那么存在以下关系:
1、L=S.length
2、L=m*P
3、以K为单位,数组S占用大小为L/8/1024
在Redis中,HyperLogLog中设置m为16834(16384个桶),P为6(每个桶占6位,只占用6位原因:bit串是64位的其中低14位用来计算桶的位置,也就是说还有50位用来得到第一个1出现的问题,如果1在最左边,那么1的位置最大也就是50,2^6 =64,是第一个大于50的数,因此6位就可以完全存下1的位置),那么L=168346(bit)。占用内存为168346/8/1024=12K。
** 第0组 第1组 ... 第16833组 **
[000 000] [000 000] [000 000] [000 000] ... [000 000]
4.3.3、做法
在Redis中,HyperLogLog首先,将保存的元素转换成对应的hash值bit串(64位)。然后根据bit串来计算应该落在桶的位置。按照从右往左的顺序,取14位(因为Redis只是将桶分为16384个=2^14 ,14位正好可以将桶完全使用上)。根据低14位计算到桶的位置之后,然后再根据剩下的50位,从右往左,找到第一个1的位置。如果1出现的位置index>oldIndex,那么index会替换掉oldIndex。否则是跳过。
参考文章
HyperLogLog在线观察** **