比bitmap节省近1000倍内存的数据结构算法——HyperLoglog

本文介绍了HyperLoglog算法,一种在大数据场景下用于高效估算基数的低内存消耗方法。通过抛硬币游戏比喻,解释了其原理,重点讨论了Redis中的优化和在实际应用中的准确性测试。算法在节省内存的同时,提供0.81%的标准误差,适合大规模UV去重统计。
摘要由CSDN通过智能技术生成

比bitmap节省近1000倍内存的数据结构/算法——HyperLoglog

HyperLoglog是什么

HyperLoglog是一个在 大数据场景 中 通过概率计算 基数的高效算法,因此在不追求绝对精确的情况下,使用HyperLoglog算是一个不错的解决方案。一般用于统计UV(独立访客,每个用户每天只记录一次),因为这种类似的指标只需要一个大概的量级,而HyperLoglog的标准误差 在0.81% ,也就是10w条数据大概只有几百条的误差,这对于大大节省内存来对比,误差率真的不算高。

好,知道HyperLoglog只能在特定场景使用,现在我们来一起开发这个需求,统计某个网站的UV(可能还要加上很多子网站),跟PV(浏览量,用户没点一次记录一次)不一样的是,它要去重,同一用户一天只能计算一次。这就要求了每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录的用户,都需要一个唯一 ID 来标识。

这时我脑海里第一反应是,去重用set啊!那就是 为每一个页面设置一个独立的 set 集合 来存储所有当天访问过此页面的用户 ID。但同样带来的问题就是:

  1. 存储空间巨大:如果网站访问量一大,你需要用来存储的 set 集合就会非常大,如果页面再一多,为了一个去重功能耗费的资源就能让服务直接宕机报OOM了!
  2. 统计复杂: 这么多 set 集合如果要聚合统计一下,又是一个复杂的事情

对于上述这样需要 基数统计 的事情,通常来说有两种比 set 集合更好的解决方案:

第一种:B 树

B 树最大的优势就是插入和查找效率很高,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。

不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 并没有节省内存

第二种:bitmap

上篇文章有提过的bitmap,一个只存放1和0的bit数组。来一条数据我们就根据ID设置为1,同时还可以做到去重。数据量变得特别大的时候, 每一个元素对应到 bit 数组中的一位

bitmap还有一个明显的优势是 可以轻松合并多个统计结果,只需要对多个结果求异或就可以了,也可以大大减少存储内存。可以简单做一个计算,如果要统计 1 亿 个数据的基数值,大约需要的内存100_000_000/ 8/ 1024/ 1024 ≈ 12 M(对bitmap不了解的可以翻看上篇文章),如果用 32 bit 的 int 代表 每一个 统计的数据(用户ID),大约需要内存32 * 100_000_000/ 8/ 1024/ 1024 ≈ 381 M

能看出来bitmap对于内存的节省显而易见,但仍然不够。统计一个对象的基数值就需要 12 M,如果统计 1 万个对象,就需要接近 120 G,对于大数据的场景仍然不适用。

概率算法:HyperLogLog

这时候HyperLogLog就出现了,概率算法 不直接存储 数据集合本身,通过一定的 概率统计方法预估基数值,这种方法可以大大节省内存,同时保证误差控制在一定范围内。HyperLogLog 的表现是惊人的,上面我们简单计算过用 bitmap 存储 1 个亿 统计数据大概需要 12 M 内存,而在 HyperLoglog 中,只需要不到 1 K 内存就能够做到!在 Redis 中实现的 HyperLoglog 也只需要 12 K 内存,在 标准误差 0.81% 的前提下,能够统计 2^64个数据

那这是怎么做到的?

二、HyperLogLog 原理

抛硬币问题

我们来玩一个游戏,设想一个抛硬币的问题,假如你抛了很多次硬币,然后说出其中连续掷为正面的最大次数,我来猜你一共抛了多少次

这很容易理解吧,例如:你说你这一次 最多连续出现了 2 次 正面,那么我就可以知道你这一次投掷的次数并不多,所以 我可能会猜是 6 或者是其他小一些的数字,但如果你说你这一次 最多连续出现了 20 次 正面,虽然我觉得不可能,但我仍然知道你花了特别多的时间。

如何估计总共抛了多少次硬币?假设1代表抛出正面,0代表反面。连续出现两次0的序列应该为“001”,那么它出现的概率应该是三个二分之一相乘,即八分之一。那么可以估计大概抛了8次硬币。

HyperLogLog也一样,通过统计二进制低位连续为零(前导零)的最大个数。通过k值可以估算集合中不重复元素的数量m,m近似等于2^k。像抛硬币的问题一样,连续出现两次0的序列,那通过我就可以猜测,大概是8次的样子。也就是m和k间存在显著的线性相关性:m 约等于 2^k

更进一步:分桶平均

因为近似m= 2^k,如果 m 介于 2^k 和 2^k+1 之间,用这种方式估计的值都等于 2k,这明显是不合理的。这时我们就可以使用多个类似这样的计算,来进行加权估计,就可以得到一个比较准确的值了。这个过程有点类似于选秀节目里面的评分,一堆评委在评分,一般都会去掉一个最高分和最低分,然后 再计算平均值,这样的出来的分数就差不多是公平公正的了。

而在真实的 HyperLogLog 要比上面的示例代码更加复杂一些,也更加精确一些。

也就是把数据分成m个均等的部分,分别估计其总数求平均后再乘以m,称之为分桶

分桶的做法是,将每个元素的hash值的二进制表示的前几位用来指示数据属于哪个桶,然后把剩下的部分再按照之前的做法处理。

假设有一个集合{ele1,ele2},其hash值的二进制表示如下:

hash(ele1) = 00110111

hash(ele2) = 10010001

如果要分两个桶,只要取元素hash值的第一位来进行分桶,剩下的部分进行前导零的计算(也就是连续出现0的次数)。

基数估计:桶数×2的(前导零+1的均值)次幂=2×2ˆ((2+3)/2)。

上述就是LogLog算法的基本思想,LogLog算法是在HyperLogLog算法之前提出的一个基数估计算法,HyperLogLog算法其实就是LogLog算法的一个改进版。

在这里插入图片描述

其中m代表分桶数,R头上一道横杠的记号就代表每个桶的结果(其实就是桶中数据的最长前导零+1)的均值,LogLog算法还乘了一个常数constant进行修正。

而优化后的HyperLogLog算法用调和平均数来改进LogLog算法,推导起来有点复杂,感兴趣可以上网搜索。

那前面说了,在 Redis 中实现的 HyperLoglog 也只需要 12 K 内存,因为在 Redis 的 HyperLogLog 实现中,用的是 16384 个桶,即:2^14,最后算出来的结果就是12KB。

三、Redis对HyperLogLog的优化

从上面我们算是对 HyperLogLog 的算法和思想有了一定的了解,并且知道了一个 HyperLogLog 实际占用的空间大约是 12 KB,但 Redis 对于内存的优化非常变态,当 计数比较小 的时候,大多数桶的计数值都是 ,这个时候 Redis 就会适当节约空间,转换成另外一种 稀疏存储方式,与之相对的,正常的存储模式叫做 密集存储,这种方式会恒定地占用 12 KB。当然,还有源码里面一系列的优化手段,以后有机会会深聊下去。

四、HyperLogLog 的使用

好了,回到需求。redis hyperloglog提供的指令有三个:pfadd添加数据,pfcount获取计数,pfmerge累加计数。和redis基本类型set的部分功能类似,其中pfadd指令可以类比set集合的sadd指令,pfcount可以类比set集合的scard指令。

在这里插入图片描述

我们可以用 Java 编写一个脚本来试试 HyperLogLog 的准确性到底有多少:

    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.80.102", 6379);
        for (int i = 0; i < 100000; i++) {
            jedis.pfadd("lujs123", "user" + i);
        }
        long lujs123 = jedis.pfcount("lujs123");
        System.out.printf("%d %d\n", 100000, lujs123);
        jedis.close();
    }

结果输出如下:

在这里插入图片描述

发现 10 万条数据只差了 275,按照百分比误差率是 0.275%,对于巨量的 UV 需求来说,这个误差率真的不算高。

在这里插入图片描述

同时观察内存情况,只用了10几K。绝了!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值