【Redis笔记】一起了解 Redis 中的 HyperLogLog 算法?

一起了解 Redis 中的 HyperLogLog 算法?


如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里

  • 前提概念

    • 需求
    • 什么是 HyperLogLog ?
    • 什么是基数?
    • 基数统计
  • 应用

    • 需求
    • 应用
  • 原理

    • 伯努利试验
    • 原理
  • Redis 实践

    • Redis HyperLogLog 与伯努利试验的联系?
    • Redis HyperLogLog 的实现?

前提概念


什么是 HyperLogLog?

HyperLogLog
HyperLogLog 是一种算法,其来源于论文 《HyperLogLog the analysis of a near-optimal cardinality estimation algorithm》,是 LogLog 算法的升级版本,主要的目的就是利用统计学原理做近似基数统计。优势就是利用一个较小的固定大小空间计算任意大小的 Distinct Value
在这里插入图片描述

Redis 的实践
HyperLogLog 并非 Redis 一家独有,Redis 只是基于 HyperLogLog 算法实现可一个 HyperLogLog 数据结构,并用该数据结构提供基数统计的功能。其优势就是可以做到只需要 12 kb 的空间大小,就可以实现接近 2^64 量级的基数统计。

HyperLogLog 数据结构并不会保存真实的元数据,所以其核心就是基数估算算法
在工程实践中,通常会用于 App 或页面的 UV 统计


什么是基数?
什么是集合?

在了解数学概念上的基数之前,我们先来了解下在数学上,是怎么描述 “集合” 的。

集合Set), 也成集,是集合论的主要研究对象。集合是指具有某种特定性质的具体或抽象的对象汇总而成的集体。其中构成集合的这些对象则称之为该集合的元素

集合特性

  • 确定性
    给定一个集合,任给一个元素,该元素只可能属于或不属于该集合。不存在模棱两可的情况
  • 互异性
    一个集合中,任何两个元素都可以被认为是不相同的,即每一个元素只会在集合中出现一次,具有去重性质
  • 无序性
    一个集合中,所有元素的地位都是相同的,元素之间是无序的

在本文中重点记住,集合中的元素是具互异性的

什么是基数?

基数 (cardinal number) 在数学上,是集合论中刻画任意集合大小的一个概念。通俗点讲,基数就是对集合元素的计数

  • 结合对集合的理解,大白话就是,基数就是指一个集合中不同元素的个数

  • 元素集合可以是有限集合或无限集合,如果是有限集合,那么集合的基数就是一个特定的自然数;如果是无限集合,那么其基数就不是一个自然数了

  • 基数(数学)-@wiki

基数?集合大小

我们知道基数就是对集合元素的计数,那说白了不就是集合元素的个数吗?为什么要引申出基数的概念,不直接使用集合大小?

这里直接贴上知乎 @lvony 的回答

  • 基数就是严格意义上的集合元素多寡,为什么不直接用集合大小或者元素个数来描述,我觉得主要是所有集合的基数集合不是自然数集合的子集。换言之所有的自然数都是基数,但不是所有的基数都是自然数
  • 如无穷集合的基数都不是自然数

基数统计

我们可以知道,基数可以简单理解成集合中不同元素的个数。那么什么是基数统计呢?在工程实践大数据领域,要精确的计算基数是十分困难的。为什么这么说呢?假设你要实现大数据集合的精确基数统计,通常我们会有两种思路

Set
我们可以采用集合的方式去统计元素基数,需要存储数据的元数据。假设有 10 亿个用户,每个用户占用 64 位的空间,那么总共就需要 1000000000 * 64 / 8 / 1024 / 1024 /1024 = 7.45 GB。似乎咋一看还能接受,但是这只是单场景的基数统计,如果存在多个场景?时间范围再放大,那么空间就会跟随基数的增长而线性增长,这肯定是不能接受的

BitMap
那么既然 Set 占用空间过大,那么 BitMap 如何?位图倒是一个可以考虑的选项,将数据经过 hash 得到位图上的索引,并将该索引标记为 1,最后经过统计位图中 1 的个数,就可以做到基数统计。但是呢,也是有缺点的,因为采用的是哈希算法,那么必然存在哈希冲突,所以位图统计也并非是一个严谨的准确基数统计。为了降低冲突,必然会将位图的大小大于集合基数的一定倍数。

假设仍然是 10 亿用户数,假设位图的大小是基数的 2 倍,那么需要的空间就是 1000000000 * 2 / 8 /1024/1024 = 238 MB, 这相比 Set 的空间占用,似乎已经可以接受了

概率算法
综上,位图的确是一个不错的选择,只需要 MB 级的空间就可以做 10 亿级别的基数统计。
但是如果有人跟你说它只需要 KB 级别的空间就可以做跟你同等量级甚至更大的基数统计呢?那位图还香吗? 在不追求绝对准确的情况下,倒是有很多基于概率算法的基数统计解决方案,比如

  • Linear Counting
  • LogLog Counting
  • HyerLogLog Counting
  • HyperLogLog++

而 Redis 则是采用 HyperLogLog 这种算法去实现的


应用


需求
  • 统计页面 UV
    • 统计 APP 或页面,每天被点击/浏览/观看的用户数量
    • 注意是 UV, 非 PV
  • 不能占用大量空间,即非 O(n) 的空间复杂度

应用

Redis 原生提供了 HyperLogLog ,它是 LogLog 算法的升级版本,可以为我们提供 “非精准的去重计数估算”,常用于 APP 或页面的 UV 统计

  • Redis Command
    在这里插入图片描述
    Redis 只为 HyperLogLog 提供了三个命令,所以还是相当简单的,我们一起来看一下
  • pfadd key element [element...]
    O(1) 的插入时间复杂度, 向指定 key 的 HyperLogLog 数据结构插入一或多个元素
  • pfcount key [key...]
    查询某 key 在 HyperLogLog 数据结构中的近似基数,如果该 key 不存在,则为 0
  • pfmerge destkey sourcekey [sourcekey...]
    将多个 HyperLogLog 合并到一个 HyperLogLog

原理


伯努利试验
什么是伯努利试验?

伯努利试验 (Bernoulli trial)
伯努利试验 (Bernoulli Trial) 是在相同的条件下,重复地,互相独立地进行的一种随机试验

  • 该随机试验只有两种互斥结果:成功失败
  • 设试验只有 2 个可能结果: A A A A ‾ \overline{A} A ,则称实验为 E E E 伯努利试验;设 P ( A ) = p ( 0 < p < 1 ) P(A) = p (0<p<1) P(A)=p(0<p<1) , 此时 P ( A ‾ ) = 1 − p P (\overline{A} ) = 1 − p P(A)=1p

伯努利过程(Bernoulli process)
假设该试验独立重复地进行了 n 次,那么我们就称这一过程的随机试验为 「n 重伯努利试验」, 也称之 「伯努利过程」

单次伯努利试验是没有价值的,所以通常我们会反复进行多次伯努利试验,构成伯努利过程,然后观察其成功了多少次,失败了多少次

推论

设在一次试验中,事件 A 发生的概率为 p ( 0 < p < 1 ) p (0<p<1) p(0<p<1), 则在 n 重伯努利试验中

  • 事件 A 恰好发生 k 次的概率为: P n ( k ) = C n k p k ( 1 − p ) n − k ; P_{n}(k) = C^k_np^k(1-p)^{n-k}; Pn(k)=Cnkpk(1p)nk; ( k = 0 , 1 , 2 , 3 , . . . , k ) (k=0,1,2,3,...,k) (k=0,1,2,3,...,k)
  • 事件 A 在第 k 次试验才首次发生的概率为 p 1 ( 1 − p ) k − 1 p^1(1-p)^{k-1} p1(1p)k1 ; ( k = 0 , 1 , 2 , 3 , . . . , k ) ;(k=0,1,2,3,...,k) ;(k=0,1,2,3,...,k)
典型案例

我们以 “抛硬币” 为一次伯努利试验举例,每次抛硬币,只有"正面""反面"两种结果

假设每次试验抛一枚硬币,我们重复进行 50 次这样的试验,那么这就构成了一轮伯努利过程


原理

伯努利过程

HyperLogLog 的实现是基于伯努利试验的结论实现的,所以要了解 HyperLogLog 的原理,我们得先回顾一下伯努利过程以及估算公式,这里还以经典案例 “抛硬币” 来举例

抛硬币只有两种结果,正面或反面,概率都是 50%, 我们假设抛掷到正面为成功,抛掷到反面为失败,对其进行实验,实验内容如下

  • 掷一次硬币为一次 「伯努利试验」
  • 假设我们以 100「伯努利试验」 为一轮 「伯努利过程」
  • 进行 10「伯努利过程」

因为每轮过程都进行了 100 次的试验(掷硬币),所以我们需要记录每轮过程中,首次出现正面时,是该轮过程的第几次试验(掷硬币),标记为 k

  • 第一轮过程首次出现正面时,所进行的试验次数,为 k1
  • 第二轮过程首次出现正面时,所进行的试验次数,为 k2
  • 以此类推,第十轮过程首次出现正面结果时,所进行的试验次数为 k10

那么对于这进行了 10 轮的伯努利过程们而言,每轮都存一个 k 值,我们取其中最大的 k ,作为最大抛掷次数,记为 k_max

  • 在 n 轮的伯努利过程中,取投掷次数最大值记为 k_max,即取最大的 k 值

估算公式与优化
估算公式
熟悉了 k_max 和 伯努利过程的轮数之后,我们再来了解估算公式,即经过各种数学证明,我们可以得到 nk_max 之间的关联估算公式

  • n = 2 k _ m a x n = 2^{k\_max} n=2k_max
    • n 是伯努利过程进行的次数
    • k_max 是所有进行的 n 轮伯努利过程中,最大的抛掷次数

什么意思呢?该公式的作用是什么?

  • 即在 k_maxn 两个变量之间,知其一便知其二
  • 如我们知道了 k_max = 10, 即抛掷 10 次硬币出现第一次正面结果,那么就可以说明至少已经进行了 n = 2 10 = 1024 n = 2^{10} = 1024 n=210=1024 次伯努利过程了

但是呢,估算公式需要经过足够多的样本实验才能够进行有效的估算,即实验样本太小,误差过大,公式则没有实用价值。那么我们要怎么减少误差呢,提高估算公式的实用程度呢?

估算优化

既然估算在样本较小的时候存在误差,那么为了减小误差,我们就需要增加实验的次数,什么意思呢?有两种减小误差的方式

方式一

  • 因为只进行了 10 轮伯努利过程,样本较小,那么我们就可以增加伯努利过程的次数,比如进行 1000 次,以此减小误差
  • 这是一个好方法,但是不足够,依然可以继续努力

方式二

  • 基于方式一的前提下,以 1000 轮伯努利过程为一次 「估算实验」
  • 进行 10 次 「估算实验」取 10 次实验的均值

「估算实验」是随意取名的,防止概念混淆,仅仅是标记 1000 轮伯努利过程组成的一次实验

什么意思呢?

  • 即进行 10 组 1000 轮伯努利过程,每组实验都可以得到一个 k_max 值,我们取 10 次实验得到的 k_max 值,并求其均值
  • 当然这里的 “均值” 指的是调和平均数,而非平时我们所说的算术平均数,可以做到去极端值的影响

小结

  • 一轮伯努利过程由多次伯努利试验组成
  • 一轮伯努利过程可以得到一个 k 值
  • 多轮伯努利过程可以得到一个 k_max 值
  • 以多轮伯努利过程标记为一次实验,那么多轮实验可以得到 k_max 的均值


Redis 实践

Redis HyperLogLog 与伯努利试验的联系?

通过以上内容的介绍,我们可以知道, k_maxn 之间是可以相互推导的,知其一便知其二。那么该估算公式又如何在 Redis 的 HyperLogLog 中得到应用呢?

我们知道计算机的数据都是以二进制进行存储的,即 01010100 的二进制串的形式。而二进制的每一比特位就跟抛硬币一样,只有 01 两种结果。我们记 0 为失败,1 为成功,"即每一个二进制串,我们都可以看作是一个伯努利过程"

K 值在哪?
从最低位(从右到左)开始数,第一个 1 所在的位置就是该次伯努利过程的 k

  • 1 所在位置为伯努利过程首次获得成功的时刻。比如 1 所在的位置,从右往左数,是第 5 位,那么该次伯努利过程在第 5 次抛硬币时,首次得到正面结果。
  • 1 的右边有多少个 0,也就说明在出现正面之前,抛了多少次反面

(图片源于网络)

比特串类比
假设有一串数据, 值为 00010000, 我们类比成经典例子抛硬币,会一串比特串就是一轮伯努利过程,进行了 8 次掷硬币试验。

  • 可以得知 k = 5 ,从低位往高位数 (从右往左),第一个 1 出现在第 5 个位置上,那么该轮伯努利过程在第 5 次试验中获得正面结果

假设我们重复进行了 n 轮伯努利过程,每轮伯努利过程都会得到一个 k, 第一次记为 k1, 第二次记为 k2, 第 n 次记录为 kn,在这 n 轮伯努利过程中,假设我们最终得到 k_max = 8,那么由已知数据,我们能知道总共进行了多少次伯努利过程吗?

  • 当然可以,既然我们知道了 k_max = 8,那么根据公式 n = 2 k _ m a x n = 2^{k\_max} n=2k_max , 那么 n = 2 8 = 256 n = 2^8 = 256 n=28=256

Redis HyperLogLog 的实现?
Redis 是如何实现 HyperLogLog 的呢?它又是如何与伯努利过程联系起来?又如何去减少误差呢?

我们这里假设一个需求,需要统计当前访问 B 站首页的 UV 数量

执行过程

(一) 首选通过 Hash 函数, 将每个用户 ID 转成为 64 位的整数
(二) 因为一个 HyperLogLog 有 16834 个桶,所以我们需要知道该用户处于哪个桶中?

  • 我们就将 64 位整数的 低位 14 位截取下来,用于获得桶的编号,比如当前用户的低 14 位值是 1020,那么该用户则处于第 1020 号桶

(三) 然后我们取剩余的 50 位 数值表示一个伯努利过程,从低位往高位数,取第一个 1 出现的位置标记为 k, 并将 k 存储到 1020 号桶中

因为每个桶的大小是 6 bits, 可以表示 0 ~ 64 大小的 10 进制数,所以用户剩余数值只有 50 位,即 k 的取值范围是 1~50,那么自然不存在存储不了 k 的问题

(四) 因为桶只有 16834 个,而访问用户肯定远大于这个数值,所以 HyperLogLog 下的每个桶必然会被多次更新数值,那么更新逻辑是什么?

  • 假设 1020 号桶,此时存储的数值为 30,即该桶所记录的 k = 30
  • 此时另一个用户 B 访问,其编号是 1020 号桶,但其得到的 k 值是 35;与现桶里存储的 k 值并不相同
  • 此时我们要回想到一个桶其实就是一组 「估算实验」, 而一组 「估算实验」 势必存在多轮伯努利过程,而我们的目的求得多轮伯努利过程中的最大 k, 所以我们得知逻辑如下
    • 如果新 k 值大于桶内记录的 k 值,则用新值覆盖旧值,即更新 k_max
    • 如果新 k 值小于或等于桶内记录的 k 值, 则不动

(五) 此时如果我们要对该 HyperLogLog 进行基数统计了,要怎么办呢?

  • 那么就对每个桶的 k_max 求调和平均值,并带入估算公式中,进行计算,求得 n
  • n 就是我们需要的 B 站访问 UV
分桶

什么是分桶?
分桶就是将一块存储空间分成很多个小空间

  • 将一定大小的空间平均分为 2 14 = 16384 2^{14} = 16384 214=16384 份,我们称之为桶; 假设为 m,即 m = 16834
  • 每个桶占用 6 bits, 即 6 位,固定大小; 假设为 p, 即 p = 6
  • 那么该空间大小则为 ( m ∗ p ) / 8 / 1024 = 16834 ∗ 6 / 8 / 1024 (m * p) /8/ 1024 = 16834 * 6 / 8 / 1024 (mp)/8/1024=168346/8/1024约等于 12 kb

为什么要分桶?
我们在原理的章节上有描述过,单批次的多组伯努利过程样本依然偏小,存在一定的误差,那么我们需要增加多组 「估算实验」,并求其 k_max 均值

  • 这里的一个桶其实就相当于一组 「估算实验」
  • 所以 Redis 的 HyperLogLog 总共就有 16834 组 「估算实验」

这的确是一个比较大的数值了,自然也可以降低估算误差

多轮伯努利过程求 k_max ?

有一个点困扰了我一小段时间,就是伯努利的原理和 Redis 的实现有一段过程没有得到相互印证,就是在 Redis 中似乎没有看到多轮伯努利过程求 k_max 这个过程?

其实是有的,我们从以上信息得到几个点

  • 每次的数据输入都是一次伯努利过程,即求得某个桶的某次伯努利过程的 K 值
  • 因为 Redis 的 HyperLogLog 总共分为 16834 个桶,每个桶其实就是一个 「估算实验」
  • 每个估算实验都存在多轮的伯努利过程,我们也知道一个桶必然存在多次的数据输入,每次输入就是一轮伯努利过程,每轮过程都会计算得到一个 k 值,并与桶内所记录 k_max 进行比较,从而得到最新的 k_max

问题


12 kb 可以记录多少的数据量?

我们在上面得知 Redis 的每个 HyperLogLog 都仅占用 12 kb 的内存空间,那么这个小小的空间中,支持我们统计多少的数据量呢,比如 UV ?

  • 因为 Redis 会将输入数据转为 64 位的整数,这 64 为都会参与到计算中。所以一个 HyperLogLog 可以支持 2 64 2^{64} 264 种数值输入, 那么自然就是说可以支持 2 64 2^{64} 264 大小的数据量啦

参考资料


  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值