1.应用
Hyperloglog提供不精确的去重计数方案,如统计一个网站的UV(独立访客数),同一个用户一天之内的多次请求只能计数一次,如果使用set集合来统计的话,会非常浪费存储空间。
虽然不精确但是也不是非常不精确,标准误差是0.81%。
2.原理
1.抛硬币问题
设想一个抛硬币的问题,假如你抛了很多次硬币,最多出现了两次连续的反面,我敢打赌你抛硬币的总次数不会太多,相反,如果你和我说最多出现了100次连续的反面,那么我敢肯定扔硬盘的总次数非常的多。
如何估计总共抛了多少次硬币?假设1代表抛出正面,0代表反面。连续出现两次0的序列应该为“001”,那么它出现的概率应该是三个二分之一相乘,即八分之一。那么可以估计大概抛了8次硬币。
HyperLogLog 原理思路是通过给定 n 个的元素集合,记录集合中数字的比特串第一个1出现位置的最大值k,也可以理解为统计二进制低位连续为零(前导零)的最大个数。通过k值可以估算集合中不重复元素的数量m,m近似等于2^k。
如上图所示,给定一定数量的用户User,通过Hash得到一串Bitstring,记录其中最大连续零位的计数为4,User的不重复个数为 2 ^ 4 = 16。
2.高精度的Hyperloglog算法
显然上面的算法是非常不准确的,我们需要对其做进一步的优化。
1.分桶
最简单的一种优化方法显然就是把数据分成m个均等的部分,分别估计其总数求平均后再乘以m,称之为分桶。
分桶的做法是,将每个元素的hash值的二进制表示的前几位用来指示数据属于哪个桶,然后把剩下的部分再按照之前的做法处理。
假设有一个集合{ele1,ele2},其hash值的二进制表示如下:
hash(ele1) = 00110111
hash(ele2) = 10010001
如果要分两个桶,只要取元素hash值的第一位来进行分桶,剩下的部分进行前导零的计算,如下图所示。
基数估计:桶数×2的(前导零+1的均值)次幂=2×2ˆ((2+3)/2)
上述就是LogLog算法的基本思想,LogLog算法是在HyperLogLog算法之前提出的一个基数估计算法,HyperLogLog算法其实就是LogLog算法的一个改进版。
LogLog算法完整的基数计算公式如下:
其中m代表分桶数,R头上一道横杠的记号就代表每个桶的结果(其实就是桶中数据的最长前导零+1)的均值,LogLog算法还乘了一个常数constant进行修正。
2.调和平均数
影响LogLog算法精度的一个重要因素就是,hash值的前导零的数量显然是有很大的偶然性的,经常会出现一两数据前导零的数目比较多的情况,所以HyperLogLog算法相比LogLog算法一个重要的改进就是使用调和平均数而不是平均数来聚合每个桶中的结果。
调和平均数的结果会倾向于集合中比较小的数,x1到xn的调和平均数的公式如下:
HyperLogLog算法用调和平均数来改进LogLog算法,公式如下:
其中constant常数和m的含义和之前的LogLog算法公式中的含义一致,Rj代表(第j个桶中的数据的最大前导零数目+1)。
3.细节微调
前面所述是Hyperloglog算法的核心,但是还需要一些细微的校正。在数据总量比较小的时候,很容易就预测偏大,所以我们做如下校正:
if DV < (5 / 2) * m:
DV = m * log(m/V)
DV代表估计的基数值,m代表桶的数量,log表示自然对数,V代表结果为0的桶的数目。假设我分配了64个桶(即m=64),当数据量很小时(比方说只有两三个),那肯定有大量桶中没有数据,也就说他们的估计值是0,V就代表这样的桶的数目。
3.性能分析
测试环境:
操作系统:centos6.8
CPU:2颗单核,64bit
内存:4g
1.时间和内存分析
redis hyperloglog提供的指令有三个,pfadd添加数据,pfcount获取计数,pfmerge累加计数。和redis基本类型set的部分功能类似,其中pfadd指令可以类比set集合的sadd指令,pfcount可以类比set集合的scard指令。对其性能进行对比测试。
使用Java编写一小段测试程序:
public class MainTest {
private static final Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String VALUE_PRE = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
private static final String TEST_KEY = "test.key";
private static final String TEST_HLL_KEY = "test.hll.key";
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("输入测试条数:");
int times = scan.nextInt();
System.out.println("输入测试指令:");
String order = scan.next();
System.out.println("----start test----");
long start = System.currentTimeMillis();
if ("sadd".equals(order)) {
saddTest(times);
} else if ("pfadd".equals(order)) {
pfaddTest(times);
} else if ("pfcount".equals(order)) {
jedis.pfcount(TEST_HLL_KEY);
} else if ("scard".equals(order)) {
jedis.scard(TEST_KEY);
}
System.out.println(order + "共耗时" + (System.currentTimeMillis() - start) + "ms");
jedis.close();
}
private static void pfaddTest(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
jedis.pfadd(TEST_HLL_KEY, item);
}
}
private static void saddTest(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
jedis.sadd(TEST_KEY, item);
}
}
private static String generateString(int i) {
return VALUE_PRE + i;
}
}
首先,使用redis的redis-cli info memory命令看下当前redis的内存情况测试流程:
编译运行java程序,插入100w条记录进行测试,value大小在30个字符左右。
1.sadd指令
消耗的时间大概是15s,内存使用情况
2.pfadd指令
消耗的时间大概是15s,内存使用情况
可以看到,相同数据量下,hyperloglog在内存方面的优势是很明显的,set集合使用内存大小是107M,而hyperloglog只用了十几K。
3.scard指令
4.pfcount指令
2.误差分析
编写测试程序来测试下hyperloglog的误差。
public class HLLError {
private static final Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String VALUE_PRE = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
private static final String TEST_HLL_ERROR_KEY = "test.hll.error.key";
public static void main(String[] args) {
for (long i=100000; i<=1000000;) {
pfaddTest(i);
long pfcount = jedis.pfcount(TEST_HLL_ERROR_KEY);
System.out.printf("count=" + i + "\tpfcount=" + pfcount);
System.out.printf("\terror percentages=%.3f\n", Math.abs(i - pfcount)/(i*0.01));
i += 100000;
}
}
private static void pfaddTest(long times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
jedis.pfadd(TEST_HLL_ERROR_KEY, item);
}
}
private static String generateString(int i) {
return VALUE_PRE + i;
}
}
运行结果:
这里我们测试hyperloglog误差的平均值是0.252%,hyperloglog标准的误差是0.81%。
为什么标准的误差是0.81%?
因为Redis中用了16384个桶,HyperLogLog的标准误差公式是1.04/sqrt(m), m是桶的个数,所以在Redis中,m=16384,标准误差则为0.81%。
参考文献:
《Redis深度历险:核心原理与应用实践》