HyperLogLog固定12KB内存,而Set内存随元素数线性增长;HLL通过16384个6位桶记录前导零最大值实现常数空间,Set则需完整存储每个元素,亿级UV下HLL仅耗12KB,Set却需GB级内存。

HyperLogLog 固定 12KB 和 Set 的 O(N) 内存增长对比
因为 HyperLogLog 不存原始数据,只维护一个由 16384 个桶组成的概率摘要结构,每个桶用 6 位(bit)记录前导零最大值,总内存恒为 16384 × 6 ÷ 8 = 12KB。而 Set 必须完整保存每个去重后的元素:比如用户 ID 是字符串 "uid_123456789"(约 12 字节),1 亿个就要 ≈ 1.2GB;如果是 64 位整数,也需 800MB+。两者根本不在一个数量级。
PFADD 插入时不做元素存储,只做哈希与桶更新
PFADD 的实际行为是:对输入元素做 MurmurHash3 哈希 → 取低 14 位(即 0–16383)定位桶号 → 取高 50 位统计前导零个数 → 若该值大于当前桶记录值,则更新桶。整个过程不分配新字符串、不建 dict entry、不触发 rehash。而 SADD 每次都要在底层字典中插入一个 dictEntry,附带 key 指针、value 指针、next 指针和哈希表扩容开销。
误差可控的前提下,12KB 换来的是线性扩展能力
当你需要按天、按页面、按渠道维度分别统计 UV,且每天新增千万级独立用户时:
- 用
Set:每个维度一个 key,1000 个维度 × 80MB = 80GB 内存,Redis 实例基本不可行 - 用
HyperLogLog:每个 key 固定 12KB,1000 个 key 仅 12MB,还能用PFMERGE合并任意维度
这种常数空间特性,使得横向分片(如按日期分 key:uv:20261020:home、uv:20261020:search)在内存层面完全无压力。
别忽略 PFMERGE 的合并代价是零拷贝
PFMERGE destkey src1 src2 ... 不是把所有源 key 的元素拉出来再塞进 dest,而是直接对各源的 16384 个桶取最大值,逐桶合并。10 个 HLL 合并仍只产生一个 12KB 结构。而如果用 Set 做类似操作,SUNIONSTORE 需要先读取全部源集合、去重、再写入新 dict,内存峰值可能翻倍,耗时随元素总数线性增长。
真正容易被忽略的点是:HLL 的 12KB 是硬上限,但它的误差不是均匀分布——当基数小于 1000 时,PFCOUNT 返回值可能跳变(因桶稀疏导致估计抖动);业务上若需平滑展示,得加一层小基数兜底逻辑,比如自动 fallback 到 SCARD 或采样校准。











