卡飞资源网

专业编程技术资源共享平台

「一文搞懂」Redis高级数据结构HyperLogLog

本章内容

简介

HyperLogLog是一种基数估算算法,可以用于在数据量很大的情况下,只需要使用很小的空间就能够近似的统计出所有数据的基数。

在Redis中,HyperLogLog是一种高级数据结构,可以使用该结构来进行基数估算,它的标准误差率为0.81%,这意味着即使在非常大的数据集上,它也可以提供非常准确的结果。

应用场景

HyperLogLog的主要应用场景:

  • 独立访客统计:通过收集用户IP或其他标识符,将其添加到HyperLogLog中,统计得到一个独立访客数量的估算值。
  • 活跃用户数统计:通过将用户标识符(如:用户ID)添加到HyperLogLog中,统计得到活跃用户数的近似值。
  • 布隆过滤器:通过与布隆过滤器结合使用,实现数据的高效检索,其中布隆过滤器用于判断元素是否存在于集合中,HyperLogLog用于估算集合中不重复元素的数量。
  • 统计分析:通过将数据特征或标识符添加到HyperLogLog中,统计得到不同特征对应的数量(如:兴趣爱好、商品种类等)。

常用命令

pfadd命令

  • 格式:pfadd key element [element …]。
  • 功能:向HyperLogLog中添加元素。
  • 返回值:HyperLogLog中元素发生变更时返回1;否则返回0。
  • 时间复杂度:O(1)。

pfcount命令

  • 格式:pfcount key [key …]。
  • 功能:统计基数。
  • 返回值:
    • 单个 key:返回HyperLogLog中存储的基数统计结果,如果key不存在则返回0。
    • 多个 key:返回多个key对应的HyperLogLog合并结果。内部处理:创建一个临时HyperLogLog合并多个key对应的HyperLogLog。
  • 时间复杂度:O(n),n为key的个数。

pfmerge命令

  • 格式:pfmerge destkey sourcekey[sourcekey ...]。
  • 功能:合并多个HyperLogLog。
  • 返回值:OK。
  • 时间复杂度:O(n),n为HyperLogLog的个数。

实现原理

HyperLogLog算法的理论基础与伯努利试验有关。

伯努利实验

伯努利试验(Bernoulli experiment)是在同样的条件下重复地、相互独立地进行的一种随机试验,其特点是该随机试验只有两种可能结果:发生或者不发生。假设该项试验独立重复地进行了n次,那么就称这一系列重复独立的随机试验为n重伯努利试验,或称为伯努利概型。

伯努利实验过程

一个硬币存在正反两面,每抛出一次,正面朝上的概率为50%(即:1/2),连续抛硬币直到出现正面朝上,记录第一次出现正面朝上的位置为k。

假如连续抛出3次硬币才出现正面朝上,那么k=3,k概率为:k = 1/2*1/2*1/2(即:(1/2)^k)。

这种找出硬币正面朝上的行为可以看作是伯努利过程,连续经历n次伯努利过程,可以找到n次正面朝上的k对应的位置(k1、k2、…...、kn)。

如图所示:

通过概率统计发现,n与k之间存在一定的联系,经历n次伯努利过程,找出k中的最大值k_max,n与k的关系为:n ≈ 2^k_max。

经历5次伯努利过程找出正面朝上的k:

  • 第一次抛掷2次出现正面朝上:k=2,n=1。
  • 第二次抛掷5次出现正面朝上:k=5,n=2。
  • 第三次抛掷6次出现正面朝上:k=6,n=3。
  • 第四次抛掷10次出现正面朝上:k=10,n=4。
  • 第五次抛掷3次出现正面朝上:k=3,n=5。

按以上n与k的关系:显然2^10不等于4 ,出现很大误差。因此,需要通过优化算法来减小误差。

估值优化

估值优化方式:

  • 增加伯努利过程轮数,取平均值。假设以x次伯努利试验为一轮,得到每轮找出的k_max,经历m轮伯努利试验后,平均值为:k_average = (k_max_1 + k_max_1 …... + k_max_m)/m。
  • 增加修正因子,修正因子是一个不固定的值,会根据实际情况来进行值的调整。这种方式涉及很多数学理论知识,感兴趣的同学可自行查阅相关资料。

以上增加伯努利过程轮数,取平均值的方式就是LogLog算法的实现,其计算公式:

其中:

  • DVLL:伯努利试验次数。
  • constant:修正因子。
  • m:伯努利试验轮数。
  • 头上带一横杠的R:平均值。

HyperLogLog与LogLog的区别是HyperLogLog使用了调和平均数(即:倒数平均数),调和平均数相比平均数的优点是能降低极大值对平均值的影响,其计算公式:

HyperLogLog实现原理

HyperLogLog处理流程大致分为三步:计算bit串、分组、对应分组。

1)计算bit串

如图所示:

图中,任一key经过hash后得到一个64bit的bit串,其中:

  • 低14bit作为分组编号,共2^14 = 16384个分组。
  • 剩余50bit作为基数估计,从低位到高位(即:从右至左)找出第一个1的位置,记为k,与当前分组的记录的k_current进行比较,如果k大于k_current,则更新;反之,不做处理。

2)分组

在Redis中,通过采用分组(分轮)策略来提高估值的准确率,将一个以bit为单位、长度为L的大数组S,平均分为m组(即:m轮),每组占有相同的比特个数,设为p。得出以下关系:

  • L = S.length
  • L = m * p
  • 以K为单位,S(内存占用)= L / 8 / 1024。

在Redis中,HyperLogLog设置为:m=16834,P=6,L=16834 * 6。占用内存 = 16834 * 6 / 8 / 1024 = 12K。

每组用6bit存储k_max的原因:6bit可以存储的最大值为63(即:6bit全部为1),目前计算机一般为64位或32位操作系统,因此6位最节省内存,又能满足需求。

如图所示:

3)对应分组

假设某个key经过hash后得到的bit串为0011001...010101000 00000000000110。其中:

  • 低14bit(00000000000110)转换为10进制得到的值为6,即:分组编号为6,对应第6个分组。
  • 剩余50bit(0011001...010101000)中,第一个1的位置为4(即:k_max=4),与当前分组编号比较:4<6,不做处理,即:该key对应第6个分组。

统计访问页面的独立用户数

假设要统计访问某个页面的独立用户数,每个用户存在一个唯一的用户ID,将用户ID通过hash散列到不同的分组中,每个分组对应一个k_max,最后将k_max代入公式中得到估算值。估算公式:

内存优化

如果每个key都占用12KB的存储空间,对于那些需求远小于12KB的key,会造成存储空间的浪费。因此,在Redis中,为了节省内存空间,在HyperLogLog内部会采用不同的编码方式进行存储:

  • 稀疏编码(默认):应对数据量小的场景。
  • 密集编码:应对数据量大的场景。

HyperLogLog底层采用SDS结构存储数据,为了区分普通字符串,在HyperLogLog头部使用了固定魔法字符串:HYLL。

满足以下任一条件时,稀疏编码会转换为密集编码:

  • HyperLogLog中任一分组的计数值(即:k_max)大于32。
  • HyperLogLog中存储内容大小超过参数hll-sparse-max-bytes(默认为3000字节)设定的值。

使用示例

客户端示例:

127.0.0.1:6379> pfadd page user1                # 添加key
(integer) 1
127.0.0.1:6379> pfcount page                    # 估算结果
(integer) 1
127.0.0.1:6379> pfadd page user2 user3 user4
(integer) 1
127.0.0.1:6379> pfcount page
(integer) 4
127.0.0.1:6379> pfadd page user2 user3 user1    # 添加已经存在的key
(integer) 0
127.0.0.1:6379> pfcount page
(integer) 4
127.0.0.1:6379>

代码实例:

/**
 * HyperLogLog基数统计
 * @return 估算结果
 */
@Override
public long hyperLogLog(){
    for (int i = 0; i < 100000; i++) {
        redisUtil.pfAdd("page","user" + i);
    }
    long result = redisUtil.pfCount("page");
    log.info("用户总数:" + 100000 + ",统计结果 : " + result);
    return result;
}

返回结果:

用户总数:100000,统计结果 : 99785

【阅读推荐】

更多精彩内容,如:

  • Redis系列
  • 数据结构与算法系列
  • Nacos系列
  • MySQL系列
  • JVM系列
  • Kafka系列

请移步【南秋同学】个人主页进行查阅。内容持续更新中......

【作者简介】

一枚热爱技术和生活的老贝比,专注于Java领域,关注【南秋同学】带你一起学习成长~

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言