HyperLogLog 是什么?
HyperLogLog 是 Redis 中的一个数据结构,只需要 12KB 大小的内存就能统计 2^64 个数据,同时也具有一定误差,大概在 0.81%左右
HyperLogLog 出处
引入 伯努利试验 ,抛出硬币多次后出现正面,总次数记为 k 次,硬币正面次数记为 n 次:
结合极大似然估算的方法,发现在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
这里的公式带入结果我们发现并不相等,因为 n 的试验次数小导致误差也很大
初高中我们常常减少误差的方法用到了平均数,那么假如 上面 3 组为一轮,进行上百轮试验,再取每轮的 k_max 最后除以 m(轮数)得到
这样得到的误差就很小了,所以通过估算优化,这就是 LogLog 的做法。
就是多轮试验的平均数。这种通过增加试验轮次,再取
k_max平均数的算法优化就是LogLog的做法
但是明显平均数在极限情况下会受到极大数极小数的影响,所以这里就需要一个新的减少误差方法 :调和平均数
这里就差不多讲清楚 HyperLogLog 的出处。
HyperLogLog 原理
结合问题看HyperLogLog跟我的需求有什么关联
统计 APP或网页 的一个页面,每天有多少用户访问进入的次数。同一个用户 ip 的反复进入就记为 1 次
HyperLogLog 总共用了 3 个步骤
1. hash 化为比特串
将传入的数据通过 hash 函数转换成 比特串,至于为什么要变成比特串,因为需要跟翻硬币联系起来。
硬币的正反正好代表着 01 标识,0 代表反面,1 表示正面。
2. 分桶
通过存储抽象化,存储的是一个以单位是比特(bit),长度为 L 的大数组 S ,将 S 平均分为 m 组,注意这个 m 组,就是对应多少轮,然后每组所占有的比特个数是平均的,设为 P
得到一个关系:
- L = S.length
- L = m * p
- 以 K 为单位,S 占用的内存 = L / 8 / 1024
在 Redis 中,HyperLogLog设置为:m=16834,p=6,L=16834 * 6。占用内存为=16834 * 6 / 8 / 1024 = 12K
第0组 第1组 .... 第16833组
[000 000] [000 000] [000 000] [000 000] .... [000 000]
3. 对应
每个用户 IP 通过哈希函数转换为一个比特串(hash( IP ))。不同的用户ID将产生不同的比特串,每个比特串至少包含一个1,所以这可以类比为一次伯努利试验。
通过比特串的前几位(例如低两位)来确定用户所属的桶(或称为“轮”)。桶的编号由这些位转换为十进制数决定。
以二进制形式的比特串为例,如果比特串是1001011000011,其低两位是11,转换为十进制是3,这意味着用户属于第3个桶。
在确定桶号后,剩下的比特串用于确定k_max,即比特串中从低位到高位第一次出现1的位置。在例子中,剩下的比特串是10010110000,第一次出现1的位置是5,所以k_max=5。
k_max的值转换为二进制(在这个例子中是101),如果桶的比特位数p大于等于3,这个值就可以存入桶中。 ps: p = 6
通过将所有用户ID分散到不同的桶中,并记录每个桶的k_max,可以估算出APP主页(key为main)的点击量。当需要统计点击量时,结合所有桶中的k_max值,代入估算公式,得出估算值。
Redis 中 HyperLogLog 实现过程
由上述可知,Redis 将桶分为 16834 (2^14)个桶,每个桶里面有 6 个比特位。
当 Redis 调用 pfadd key value时, value 被 hsah 成 64 位比特串时,我们首先取低位开始的 14 位( 从右到左 )作为选择桶位( index ),至于为什么选 14 位,主要是因为 2^14 可以刚好的把 16834 个桶全部用完不造成浪费。
( 00 0000 0000 0010 )2 => ( 2 )10
所以会被放入第 2 个桶里面
然后剩下 50 位比特串,就通过上面说的从低位找首次出现的 1 的位置
( 00 1000 1001 ···· 0001 0000 0000 0000 )
上面就得到了 index = 13,在桶中表示范围是 0 ~ 63,即最大可以存入数据为 63
如果桶中已经存入,只需要将大的保留在桶中即可
现在我们又看一看极限情况
( 10 0000 0000 ···· 0000 0000 0000 0000 )
index = 50,显然最大都能存,其他情况说明都可以容纳到桶里面进行比较
最后当 Redis 调用 pfcount key时,就会调用上面讲到的 DV公式,根据桶中的 k_max 值,得到最终结果。
HyperLogLog 优化部分
看到这儿就有疑惑了,还有什么优化?
上面公式中,提到的 constant 并不是一个常量值,在实际的 HyperLogLog 计算中,它是一个分支函数
同时还有一个关系式
// m 为桶数
switch (p) {
case 4:
constant = 0.673 * m * m;
case 5:
constant = 0.697 * m * m;
case 6:
constant = 0.709 * m * m;
default:
constant = (0.7213 / (1 + 1.079 / m)) * m * m;
}
其他
合并
HyperLogLog 其实还有个地方需要注意,假如多个桶数相同的数据流时,我们需要合并,没错,就是合并,这常常用于合并访问数据的需求
Redis 调用 pfmerge key3 key1 key后,将 key1,key2 数据流中 进行并集化。
例如 u1 访问了 p1 和 p2 两个页面,在 key1 ,key2 中都存在 u1 这个基值,所以并集化后只算访问了一次。
其他实现方式
hash 和 set 我就不过多阐述了,很简单就是利用了 set 的去重, hash 的 键值存储及每次访问就设置为 1,最后访问时只需要统计个数。
讲讲 BitMap 这个数据结构实现
BitMap 其实就是一个由 string 类型实现的字节数组,每个字节又对应了 8 个比特位。所以我们可以看成其实是一个 bit 为单位的数组,数组每一位用 0 或 1 表示,下标又叫偏移量
Redis 调用了SETBIT key offset value
key 就是页面,offset 就是用户 IP 转化为数字后,value 固定为 1。
由此可见就是通过将 key 编码化后得到唯一值,这种方式跟 hash 方法很像,所以也有冲突的可能,这个暂时不讨论
最后调用 Bitcount key,就可以得到在字节数组中 1 的个数,完成统计访问
最后推荐之前看的HyperLogLog文章 cloud.tencent.com/developer/a… www.cnblogs.com/linguanh/p/…