统计非重复元素个数HyperLogLog:如UV/在线人数/点赞数/注册数
HyperLogLog:
是什么:
是redis 2.8.9版本新增的高级数据结构,但核心是HyperLogLog算法(基数估算算法)。基数估值计数(cardinality counting)通常用来统计一个集合中不重复的元素个数,能统计非重复元素的个数,统计大量元素时占用的空间极小。
为什么需要他:
能做什么:一般用来统计非重复元素的个数,还能统计多个集合中非重复元素的个数,数据量最大支持2^64,但占用空间最大仅12K
使用场景:
如统计UV/在线日活/月活(每次的集合进行merge既月活)/点赞数/注册数/关键词搜索数量/登录ip/注册ip
优缺点:
优点:
1.最大统计元素个数为2^64次,但占用空间仅12k
2.统计大量数据时,误差率极小,官方的误差为0.81%,但在实际测试中误差为1%,数据量为1KW
3.在存储时并不会一次性占用12K空间,而是随着数据量的增大逐渐增大占用空间,但使用pfmerge会直接占用12k(这由hyperloglog合并操作的原理(两个hyperloglog合并时需要单独比较每个桶的值)可以很容易理解。)
4.对3的补充:Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间
5.目的是做基数统计,故不是集合,不会保存元数据,只记录数量而不是数值,所以占用空间极小
6.还可以实现多个集合去重统计元素个数
缺点:
1.并不存储元素本身,所以无法取出存入的元素,仅可用来去重计数
2.因为是概率统计法,所以会存在误差
3.基数不大,数据量不大就用不上,会有点大材小用浪费空间
和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
设计理念:
通过概率算法不直接存储数据集合本身,通过一定的概率统计方法预估基数值,这种方法可以大大节省内存,同时保证误差控制在一定范围内。
目前用于基数计数的概率算法包括:
Linear Counting(LC):早期的基数估计算法,LC在空间复杂度方面并不算优秀,实际上LC的空间复杂度与上文中简单bitmap方法是一样的(但是有个常数项级别的降低),都是O(Nmax);
LogLog Counting(LLC):LogLog Counting相比于LC更加节省内存,空间复杂度只有O(log2(log2(Nmax)));
HyperLogLog Counting(HLL):HyperLogLog Counting是基于LLC的优化和改进,在同样空间复杂度情况下,能够比LLC的基数估计误差更小。
实现细节:
参考:
https://www.cnblogs.com/lh-ty/p/9972901.html
http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html
https://www.jianshu.com/p/e40fe9c581ad
HLL中实际存储的是一个长度为mm的大数组SS,将待统计的数据集合划分成mm组,每组根据算法记录一个统计值存入数组中,redis中这个数组的大小是16834,m越大,基数统计的误差越小,但需要的内存空间也越大。
1.将元素转为hash并进行hash重运算(MurmurHash64A)生成一个比特串
2.比特串的低t位对应的数字用来找到数组S中对应的位置i
3.从比特串t+1位开始找到第一个1出现的位置k,将k记入到数组Si中,这里引入了分桶计算的概念,来保证值得平均性。
4.基于数组S记录的所有数据的统计值,计算整体的基数值,
基数统计:只统计集合中不重复元素的个数。
伯努利试验(Bernoulli trial)就是结果有且仅有两种可能性的单次随机试验。
伯努利过程:一系列的互相独立并且结果概率相同的伯努利试验就是一个伯努利过程。
得出:根据最大值可以推导出需要重复的次数既基数个数,也可以反向推导根据基数的个数可得知最大值。公式: N=2^kmax
我们要统计一组数据中不重复元素的个数,既想要获取的是该元素的基数,那么可以通过kmax去推导:
集合中每个元素的经过hash函数后可以表示成0和1构成的二进制数串,一个二进制串可以类比为一次抛硬币实验,1是抛到正面,0是反面。二进制串中从低位开始第一个1出现的位置可以理解为抛硬币试验中第一次出现正面的抛掷次数k,那么基于上面的结论,我们可以通过多次抛硬币实验的最大抛到正面的次数来预估总共进行了多少次实验,同样可以通过第一个1出现位置的最大值$ k_{max}k max来预估总共有多少个不同的数字(整体基数)。
redis具体是如何做的:
例:比如存储一个元素 "我是一个字符串"
1.将字符串 通过MurmurHash64A 函数 计算一个64位的hash值,64位的前14位(这个值是可以修改的)作为index,后面作为50位作为bit流。
2.计算后50位第一次出现1的位置,其中第一次出现1的位置我们记为count, 所以count最大值是50, 用6个bit位就够表示了。
3.更新桶的操作:根据index找到桶,然后看当前的count 是否大于oldcount,大于则更新下oldcount = count。
此时为了性能考虑,是不会去统计当前的基数的,而是将HLL的头里面的一个标志位置为1,表示下次进行pfcount操作的时候,当前的缓存值已经失效了,需要重新统计缓存值。在后面pfcount流程的时候,发现这个标记为失效,就会去重新统计新的基数,放入基数缓存。
如何使用:
1.redis使用
pfadd 时间复杂度O(1): 增加元素,成功则返回1,失败返回0,如果存储多个元素中只有一个不重复,也会返回1,既判定存入成功。
pfadd key values
pfcount 时间复杂度为O(N): 判断当前集合的元素数量
pfcount key
pfmerge 时间复杂度为O(N): 将多个集合的元素放到同一个集合中且进行去重,并且一次性占用空间为12K
pfmerge destkey sourcekey1 sourcekey3 ..
2.java如何使用HyperLogLog
参考:https://segmentfault.com/a/1190000010531547
<dependency>
<groupId>net.agkn</groupId>
<artifactId>hll</artifactId>
<version>1.6.0</version>
</dependency>
@Test
public void testSimpleUse(){
final int seed = 123456;
HashFunction hash = Hashing.murmur3_128(seed);
// data on which to calculate distinct count
final Integer[] data = new Integer[]{1, 1, 2, 3, 4, 5, 6, 6,
6, 7, 7, 7, 7, 8, 10};
final HLL hll = new HLL(13, 5); //number of bucket and bits per bucket
for (int item : data) {
final long value = hash.newHasher().putInt(item).hash().asLong();
hll.addRaw(value);
}
System.out.println("Distinct count="+ hll.cardinality());
}
问题:
HyperLogLog 既然可以去重,那在逻辑上应该也能提供判断元素是否存在的功能吧?
不能,因为他没有真实的去存储元素,并且使用了基数算法,只是根据元素特性在其结构内增加统计次数,然后根据次数去估算基数值。