Redis--基础知识点--29--HyperLogLog

Redis 的 HyperLogLog 是一种用于基数统计的概率数据结构。它可以在极小的内存开销下(每个键约 12 KB)估算一个集合中不重复元素的个数(即基数),标准误差为 0.81%。

为什么需要 HyperLogLog?

在传统方案中,统计唯一元素(如独立访客数 UV、唯一搜索词数)通常用 Set 或 Hash 存储,当数据量巨大时(如千万级 IP),内存会迅速膨胀。HyperLogLog 在牺牲少量精确度的前提下,将内存占用从 O(N) 降低到 固定 12KB,非常适合超大数据集的基数估算场景。

工作原理简述

HyperLogLog 基于概率计数 和伯努利试验的观察:

  • 对每个元素使用哈希函数得到二进制串,统计该串中第一个 1 出现的位置(前导零数)。
  • 通过多个桶(分桶平均)来抵消偶然性,最后用调和平均得到整体基数的估计值。

Redis 实现中使用了 16384 个桶,每个桶用 6 位存储(共 16384 × 6 = 12KB),标准误差约 0.81%。

Redis 中的 HyperLogLog 命令

命令 说明
PFADD key element [element ...] 将元素添加到 HyperLogLog 中。如果内部存储变化返回 1,否则返回 0。
PFCOUNT key [key ...] 返回一个或多个 HyperLogLog 的估算基数。多个 key 会合并后再计算。
PFMERGE destkey sourcekey [sourcekey ...] 将多个 HyperLogLog 合并到 destkey 中,结果相当于它们的并集。

注意 :命令前缀 PF 取自 HyperLogLog 的发明人 Philippe Flajolet 的名字。

使用示例

bash 复制代码
# 统计某网页的 UV(独立访客)
127.0.0.1:6379> PFADD page1:uv user1 user2 user3
(integer) 1
127.0.0.1:6379> PFADD page1:uv user2 user3 user4
(integer) 1
127.0.0.1:6379> PFCOUNT page1:uv
(integer) 4   # 实际去重后 user1,user2,user3,user4 共 4 个

# 合并多个页面的 UV 得到整个网站的 UV
127.0.0.1:6379> PFMERGE site:uv page1:uv page2:uv
OK
127.0.0.1:6379> PFCOUNT site:uv
(integer) 估算值

适用场景

  • 独立访客数(UV)统计:每日访问网站的 IP 或用户 ID 去重计数。
  • 注册 IP 去重统计:分析唯一来源 IP。
  • 搜索词唯一性统计:计算有多少个不同的搜索词。
  • 大数据流中计算 distinct count:如日志分析、用户行为埋点。

重要注意事项

  1. 不是精确计数:误差约 0.81%,适合允许少量误差的大规模统计场景。需要精确值时请用 Set 或 Bloom Filter 变种。
  2. 无法删除元素 :HyperLogLog 是只增结构,不支持从统计中移除某个元素(无 PFREM 命令)。
  3. 小数据量时精确:当基数较小时(< 几千),Redis 内部会使用稀疏编码,实际计数是精确的,达到阈值后才转为估算模式。
  4. 合并性能 :PFMERGE 会读取所有源 key 并写回目标,复杂度 O(N),大 key 需注意耗时。

与其他结构的对比

结构 内存占用 是否精确 支持删除 典型用途
Set O(N) 元素数 精确 是 小规模去重集合
Bitmap 取决于最大值 精确 否 用户签到、布尔状态
Bloom Filter 较小(有误判) 否(有假阳性) 否 存在性过滤
HyperLogLog 固定 12KB 估算(0.81% 误差) 否 超大规模基数统计

总结

HyperLogLog 是 Redis 中一个非常轻量级的基数估算工具,它以 12KB 的固定内存和 0.81% 的误差,解决了海量数据去重计数的痛点。适用于 UV、独立 IP、唯一词条等场景,是在精确性和内存之间取得良好平衡的工程方案。



工作原理详解:

了解即可

好的,我们抛开官方术语,从最直观的概率小实验开始讲起,一步一步让你真正理解 HyperLogLog 是如何"算"出基数的。


第一步:一个抛硬币的奇妙现象

想象你在抛一枚均匀硬币 ,记录第一次出现正面(记作 1)之前连续抛出了多少个反面(记作 0)。

  • 抛一次就得到正面:连续 0 的个数 = 0
  • 先反面再正面:01 → 连续 0 的个数 = 1
  • 先两个反面再正面:001 → 连续 0 的个数 = 2
  • 先三个反面再正面:0001 → 连续 0 的个数 = 3

你会发现:连续 0 的个数越大,这件事发生的概率就越低。

具体来说,连续 k 个 0 的概率是 1/2^(k+1) 。

所以,如果你在实验中观察到了连续 3 个 0 ,你就有理由相信:你大概进行了 2^(3+1)=16 次 抛掷(因为要等这么久才出现一次"001"这个模式)。

核心思想:一个罕见事件的发生,暗示了实验总次数很大。

观察到的"最长连续 0 的位数"可以用来估计总抛掷次数。


第二步:把抛硬币换成哈希函数

对于集合中的每一个元素(比如用户 ID),我们用一个哈希函数 把它变成一个二进制串 (比如 32 位或 64 位)。

这个二进制串可以看作是一长串的"硬币抛掷结果"------0 是反面,1 是正面。

我们规定:从二进制串的低位(最右边)开始数,看第一个 1 之前有多少个 0 。

这个数叫做 ρ(rho)。

  • 如果哈希值是 ...1010,末尾是 0,再往前是 1,所以 ρ = 1(只有一个尾随 0)。
  • 如果哈希值是 ...1000,末尾三个 0 然后 1,所以 ρ = 3。
  • 如果哈希值是 ...1111,末尾就是 1,没有尾随 0,所以 ρ = 0。

显然,ρ 越大,这个哈希值就越罕见(概率为 1/2^(ρ+1))。


第三步:单次估计为什么不准

假设我们只有一个元素,我们算出它的 ρ = 5。

那我们能否说"整个集合的基数大约是 2^(5+1) = 64"?

显然不能------因为可能只是运气好,随便一个元素就产生了很大的 ρ。

反过来,如果集合里有 10000 个不同元素,我们可能并没有观察到 ρ 很大的值(最大 ρ 才 4),那么用 2^(4+1)=32 去估计就会严重偏低。

单个样本波动太大,需要统计多个"实验"来平滑误差。


第四步:分桶平均 ------ 让大数定律帮你

HyperLogLog 把哈希值的前几位用来分桶 ,后几位用来计算 ρ。

  • 比如哈希值 64 位:前 14 位作为桶编号(2^14 = 16384 个桶),剩下 50 位用来计算 ρ。
  • 每个桶只记录这个桶里所有元素中最大的 ρ。

为什么这样做?

每个桶相当于独立的"抛硬币实验",我们把所有元素分散到 16384 个桶中,每个桶都看到了该桶内元素的最大尾随零个数。

然后我们把所有桶的估计值综合起来 ,用调和平均来得到一个更稳的总体估计。


第五步:综合公式(感性理解)

调和平均公式(简化版):

复制代码
估计基数 = 常数 × 桶数 × (  Σ 2^(每个桶的最大ρ) 的倒数  ) 的倒数

更直观的理解:

每个桶给出一个局部估计 2^(max_ρ),但这些估计值可能偏大或偏小。调和平均可以压制过大的异常值,使结果更稳定。

最终 Redis 使用的公式加上了一个偏差修正(小基数和大基数时修正不同),但核心就是基于每个桶里记录的最长尾随零,反向估算总的不重复元素数。


第六步:用一个简单例子手算

假设我们只有 4 个桶(实际 Redis 有 16384 个桶),简化演示。

集合元素经过哈希后,分布到桶 0~3:

  • 桶 0 收到的元素哈希尾随零最大值 = 2
  • 桶 1 收到的元素哈希尾随零最大值 = 1
  • 桶 2 收到的元素哈希尾随零最大值 = 4
  • 桶 3 收到的元素哈希尾随零最大值 = 0

那么每个桶的局部估计:22=4,21=2,24=16,20=1。

调和平均 = 4 / (1/4 + 1/2 + 1/16 + 1/1) = 4 / (0.25 + 0.5 + 0.0625 + 1) = 4 / 1.8125 ≈ 2.206

乘以桶数(4)再乘一个修正系数(约 0.7213)≈ 2.206 × 4 × 0.7213 ≈ 6.37

所以估算出的基数大约 6 个。

如果实际元素有 7~8 个,误差在可接受范围内。


第七步:Redis 的具体实现细节

  • 哈希函数:Redis 使用 MurmurHash2,生成 64 位哈希值。
  • 分桶:取前 14 位作为桶索引(0 ~ 16383),剩下 50 位用来计算 ρ。
  • 存储 :每个桶用 6 位存储最大 ρ(因为 50 位二进制串的尾随零最多 50,6 位足够)。
    16384 × 6 = 98304 位 = 12288 字节 = 12 KB。
  • 编码优化 :当基数很小时(比如桶里大部分还是 0),Redis 使用稀疏编码节省内存,只有超过阈值才转成 12KB 的密集编码。
  • 合并(PFMERGE) :对多个 HyperLogLog 的相同桶取 max(ρ),然后重新计算估计值。

第八步:为什么误差是 0.81%?

数学上可以证明,这种分桶调和平均方法的相对标准误差约为 1.04 / sqrt(桶数)。

Redis 的桶数 = 16384,sqrt(16384) = 128,1.04/128 ≈ 0.008125 ≈ 0.81% 。

所以你得到的结果有 95% 的概率落在真实值 ±0.81% 范围内。


第九步:直观类比总结

HyperLogLog 就像一个拥有 16384 个记分员的公司。

每个新来的用户(元素)会随机跑到一个记分员那里,并展示自己抛硬币能连续抛多少个反面(ρ)。

记分员只记住他见过的最大反面次数 。

最后所有记分员聚在一起,用每个人记住的最大反面次数,通过一个公式反推大概来了多少不同的用户。

因为每个记分员都只存一个很小的数字(最大 ρ,通常 0~50),所以总内存很小。

虽然不能精确说出人数,但误差控制在不到 1%,对于统计 UV 这种需求完全够用。


希望这个逐步拆解能让你彻底明白 HyperLogLog 的魔法。如果还有哪一步不清楚,可以继续问,我用更简单的例子或者画图解释。

相关推荐
数据库小学妹6 小时前
数据共享交换平台选型:交换方式对比与避坑指南
数据库·信创·数据同步·数据交换平台·政务数据共享·数据共享交换平台·数据库底座
loong_XL8 小时前
决策模型做内容安全检测:从踩坑到上线
数据库·安全·jev·决策模型
这个DBA有点耶8 小时前
数据库双轨并行实战:全量并行策略、增量延迟控制、双向回切与一致性校验
数据库·架构·dba
adinnet20268 小时前
保单、赔付与渠道问数:保险经营数据如何实现按需查询
大数据·数据库·人工智能
云贝贝贝9 小时前
PostgreSQL 分区表:从设计到运维,大表不再卡
运维·数据库·postgresql
oradh9 小时前
Oracle固定执行计划的方法---SQL Plan Baseline
数据库·sql·oracle
数据库小学妹9 小时前
数据库高可用演练怎么做?稳态定义、注入点与中止条件
数据库·rto·高可用架构·运维体系·故障演练·容灾切换
zyseo89 小时前
谷歌SEO 站内搜索优化实战:把站内搜索词变成关键词金矿
java·服务器·数据库
qq_401700419 小时前
Qt 串口/网口通信:Hex 与 ASCII 编码转换深度指南
开发语言·数据库·qt
码爸9 小时前
多源异构数据库实时同步至 Doris
数据库·flink·scala