• 11. 优秀的基数统计算法--HyperLogLog


    楔子

    在我们实际开发的过程中,可能会遇到这样一个问题,当我们需要统计一个大型网站的独立访问次数时,该用什么的类型来统计?

    如果我们使用 Redis 中的集合来统计,当它每天有数千万级别的访问时,将会是一个巨大的问题。因为这些访问量不能被清空,我们运营人员可能会随时查看这些信息,那么随着时间的推移,这些统计数据所占用的空间会越来越大,逐渐超出我们能承载最大空间。

    例如,我们用 IP 来作为独立访问的判断依据,那么我们就要把每个独立 IP 进行存储,以 IP4 来计算,IP4 最多需要 15 个字节来存储信息,例如:110.110.110.110。当有一千万个独立 IP 时,所占用的空间就是 15 bit * 10000000 约定于 143MB,但这只是一个页面的统计信息,假如我们有 1 万个这样的页面,那我们就需要 1T 以上的空间来存储这些数据。而且随着 IP6 的普及,这个存储数字会越来越大,那我们就不能用集合的方式来存储了,这个时候我们需要开发新的数据类型来做这件事了,而这个新的数据类型就是我们今天要介绍的HyperLogLog。

    HyperLogLog介绍与使用

    HyperLogLog(下文简称为 HLL)是 Redis 2.8.9 版本添加的数据结构,它用于高性能的基数(去重)统计功能,它的缺点就是存在极低的误差率

    HLL 具有以下几个特点:

    • 能够使用极少的内存来统计巨量的数据,它只需要 12K 空间就能统计 2^64 的数据;
    • 统计存在一定的误差,误差率整体较低,标准误差为 0.81%;
    • 误差可以被设置辅助计算因子进行降低。

    HLL 的命令只有 3 个,但都非常的实用,下面分别来看。

    添加元素

    pfadd key element1 element2······,可以同时添加多个。

    127.0.0.1:6379> pfadd hll1 mea
    (integer) 1
    127.0.0.1:6379> pfadd hll1 kano nana
    (integer) 1
    127.0.0.1:6379> pfadd hll1 mea
    (integer) 0
    127.0.0.1:6379> 
    

    统计不重复的元素个数

    pfcount key1 key2····,可以同时统计多个HHL结构。

    127.0.0.1:6379> pfcount hll1
    (integer) 3  # 不重复元素个数有3个
    127.0.0.1:6379> 
    

    将多个HLL结构中元素移动到新的HLL结构中

    pfmerge key key1 key2····,将key1、key2····移动到key中。

    127.0.0.1:6379> pfadd hll1 mea kano nana
    (integer) 1
    127.0.0.1:6379> pfadd hll2 mea kano yume
    (integer) 1
    127.0.0.1:6379> pfmerge hll hll1 hll2
    OK
    127.0.0.1:6379> pfcount hll
    (integer) 4
    127.0.0.1:6379> 
    

    当我们需要合并两个或多个同类页面的访问数据时,我们可以使用 pfmerge 来操作。

    Python实现HLL相关操作

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. pfadd key1 key2···
    client.pfadd("HLL1", "a", "b", "c")
    client.pfadd("HLL2", "b", "c", "d")
    
    # 2. pfcount key1 key2···
    print(client.pfcount("HLL1", "HLL2"))  # 4
    
    # 3. pfmerge key key1 key2···
    client.pfmerge("HLL", "HLL1", "HLL2")
    print(client.pfcount("HLL"))  # 4
    

    HyperLogLog算法原理

    HyperLogLog 算法来源于论文HyperLogLog the analysis of a near-optimal cardinality estimation algorithm,想要了解 HLL 的原理,先要从伯努利试验说起,伯努利实验指的是在同样的条件下重复地、相互独立地进行的一种随机试验,其特点是该随机试验只有两种可能结果:发生或者不发生。我们假设该项试验独立重复地进行了n次,那么就称这一系列重复独立的随机试验为n重伯努利试验,或称为伯努利概型。比如最经典、也是最好理解的抛硬币,每一次抛出的硬币都是各自独立的,当前抛出的硬币不受上一次的影响。

    注意:单个伯努利试验是没有多大意义的,然而,当我们反复进行伯努利试验,去观察这些试验有多少是成功的,多少是失败的,事情就变得有意义了,这些累计记录包含了很多潜在的非常有用的信息。

    并且根据大数定理我们知道,如果一个事件发生的概率是恒定的,那么随着试验次数的增加,那么该事件的频率越接近概率。还拿抛硬币举例,假设你抛硬币抛了四次,全是正面(这种情况是可能出现的),难道我们就说抛出一枚硬币,正面朝上的概率是百分之百吗?显然不能,而大数定理会告诉我们,只要你抛出硬币的次数足够多,你会发现正面出现的次数除以抛出的总次数会无限接近二分之一。

    之所以说这些,是因为Redis采用的算法不是按照类似我们上面说的方式,因为大数定理对于数据量小的时候,会有很大的误差。而为了解决这个问题,HLL 引入了分桶算法和调和平均数来使这个算法更接近真实情况。

    分桶算法是指把原来的数据平均分为 m 份,在每段中求平均数在乘以 m,以此来消减因偶然性带来的误差,提高预估的准确性,简单来说就是把一份数据分为多份,把一轮计算,分为多轮计算。

    而调和平均数指的是使用平均数的优化算法,而非直接使用平均数。

    例如小明的月工资是 1000 元,而小王的月工资是 100000 元,如果直接取平均数,那小明的平均工资就变成了 (1000+100000)/2=50500‬ 元,这显然是不准确的,而使用调和平均数算法计算的结果是 2/(1/1000+1/100000)≈1998 元,显然此算法更符合实际平均数。

    所以综合以上情况,在 Redis 中使用 HLL 插入数据,相当于把存储的值经过 hash 之后,再将 hash 值转换为二进制,存入到不同的桶中,这样就可以用很小的空间存储很多的数据,统计时再去相应的位置进行对比很快就能得出结论,这就是 HLL 算法的基本原理,想要更深入的了解算法及其推理过程,可以看去原版的论文,链接地址:http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf

    小结

    当需要做大量数据统计时,普通的集合类型已经不能满足我们的需求了,这个时候我们可以借助 Redis 2.8.9 中提供的 HyperLogLog 来统计,它的优点是只需要使用 12k 的空间就能统计 2^64 的数据,但它的缺点是存在 0.81% 的误差,HyperLogLog 提供了三个操作方法:pfadd 添加元素、pfcount 统计元素和 pfmerge 合并元素。

  • 相关阅读:
    Spring事务内方法调用自身事务 增强的三种方式
    SpringBoot优化内嵌的Tomcat
    Tomcat 8.0的并发优化
    Swift搭建本地http服务器,实现外部视频即时播放
    更新ruby:Error running 'requirements_osx_brew_update_system ruby-2.4.1报错解决
    iOS关于沙盒文件拷贝manager.copyItem的一个坑
    Swift udp实现根据端口号监听广播数据(利用GCDAsyncUdpSocket实现)
    iOS刻度尺换算之1mm等于多少像素理解
    Swift下的基于UIView的位置属性扩展
    iOS Main Thread Checker: UI API called on a background thread的解释
  • 原文地址:https://www.cnblogs.com/traditional/p/13326391.html
Copyright © 2020-2023  润新知