Redis高级数据结构——HyperLogLog
redis的高级数据结构HyperLogLog提供不精确的去重计数方案,虽然不精确,但是标准误差也达到了0。81%,也足以满足很多场合了。
比如,要对大型网站的每天独立访客数进行大致统计的话,就可以用到HyperLogLog了。
1.HyperLogLog的使用方法
pfadd key element [element …] #增加计数,类似于set的sadd
pfcount key #获取计数,类似于set的scard
以redis-cli命令行的方式向HyperLogLog中放入十个元素,获取计数,发现还是精确的
下面改用python脚本,放更多元素看看是否可以继续精确下去
import redis
client = redis.StrictRedis()
flag = 0
for i in range(1000):
#添加元素进去
client.pfadd("vistor", "user%d" % i)
total = client.pfcount("vistor")
#找到第一次出现统计误差时候的地方
if total != i+1 and flag == 0:
print("first appear difference:count_total = ", total,
" real_total = ", i+1)
flag = 1
#最后HyperLogLog的统计值
print(total)
可以看到,当统计量增加到1000的时候,会在加入第100个元素时第一次出现统计误差,并且最后统计的数据超过了11个,误差为1.1%。
接下来将数据再加大一些,看看误差会怎么样
import redis
client = redis.StrictRedis()
for i in range(1000000):
#添加元素进去
client.pfadd("vistor", "user%d" % i)
total = client.pfcount("vistor")
#最后HyperLogLog的统计值与实际值
print("count_total = ", total, "real_count = ", 1000000)
可以看到,加入1000000个元素后,HyperLogLog统计的的数据超出1788,误差为0.1788%。很明显,当数据量越大的时候,误差率会越低。
由此可见,拿HyperLogLog来做不完全精确去重是有效的。
pfmerge destkey sourcekey [sourcekey …] #将多个HyperLogLog合并为一个新的
2.HyperLogLog的实现原理
hyperloglog用类似图中这种方法来进行不精确计数。
给定一些列的随机整数,记录下低位连续零位的最大长度,这个参数就是图中的maxbit,通过这个k值可以估算出随机数的数量N。
现在编写简易代码做实验,观察随机整数的数量N和K值的关系。
public class pfTest {
static class BitKeeper {
private int maxbits;
//算出低位零的个数
private int lowZeros(long value) {
int i = 1;
for (; i < 32; i++) {
/*遇到了第一个1,低位连续0的个数确定为i-1
*右移i位后,低位第一个1被移出去了,再左移i位之后,
*会少一个1,此时与原值不同*/
if (value >> i << i != value) {
break;
}
}
return i - 1;
}
public void random() {
long value = ThreadLocalRandom.current().nextLong(2L << 32);
int bits = lowZeros(value);
//将maxbits更新为低位0的个数
if (bits > this.maxbits) {
this.maxbits = bits;
}
}
}
static class Experiment {
private int n;
private BitKeeper keeper;
public Experiment(int n) {
this.n = n;
this.keeper = new BitKeeper();
}
public void work() {
for (int i = 0; i < n; i++) {
this.keeper.random();
}
}
public void debug() {
System.out.printf("%d %.2f %d\n", this.n, Math.log(this.n) / Math.log(2), this.keeper.maxbits);
}
public static void main(String[] args) {
for (int i = 1000; i < 100000; i += 100) {
Experiment exp = new Experiment(i);
exp.work();
exp.debug();
}
}
}
}
可以看到K和N的对数是线性相关的,即N ≈ 2^K。
若是N介于2K和2K+1之间,用这种方式估计的值都等于2K,这肯定是不合理的。
这里采用多个BitKeeper,然后进行加权估计,就可以得到一个比较准确的值。
public class pfTest {
static class BitKeeper {
private int maxbits;
private int lowZeros(long value) {
int i = 1;
for (; i < 32; i++) {
if (value >> i << i != value) {
break;
}
}
return i - 1;
}
public void random(long value) {
int bits = lowZeros(value);
if (bits > this.maxbits) {
this.maxbits = bits;
}
}
}
static class Experiment {
private int n;
private int k;
private BitKeeper[] keepers;
public Experiment(int n) {
this(n, 1024);
}
public Experiment(int n, int k) {
this.n = n;
this.k = k;
this.keepers = new BitKeeper[k];
for (int i = 0; i < k; i++) {
this.keepers[i] = new BitKeeper();
}
}
public void work() {
for (int i = 0; i < this.n; i++) {
long m = ThreadLocalRandom.current().nextLong(1L << 32);
BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)];
keeper.random(m);
}
}
public double estimate() {
double sumbitsInverse = 0.0;
for (BitKeeper keeper : keepers) {
sumbitsInverse += 1.0 / (float) keeper.maxbits;
}
double avgBits = (float) keepers.length / sumbitsInverse;
return Math.pow(2, avgBits) * this.k;
}
public static void main(String[] args) {
for (int i = 100000; i <= 1000000; i += 100000) {
Experiment exp = new Experiment(i);
exp.work();
double est = exp.estimate();
System.out.printf("%d %.2f %.2f\n", i, est, Math.abs(est - i) / i);
}
}
}
}
代码中分了1024个桶,计算平均数使用了调和平均(倒数的平均),这样可以降低个别离群值对平均结果产生的影响。
由输出可见,误差率百分比控制在个位数,说明精度还是挺高的。
真实的HyperLogLog要比上面的代码更复杂,更精确。
Redis的HyperLogLog实现中使用的是16384(214)个桶,每个桶的maxbits需要6个bit来存储,最大可以表示maxbits=63,这样总共占用内存为214×6/8 = 12KB。即pf的内存占用为12KB。