Redis高级数据结构——HyperLogLog

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

loser与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值