Redis HyperLogLog(基数统计)
本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。
1. 定位:解决什么问题
HyperLogLog 维护集合基数的概率估计,适合活动参与人数、UV、不同 IP 等统计。Set 保存实际成员且精确,HLL 更省内存但不能查询成员、枚举成员或精确删除一个参与者。标准误差约为 0.81%,这是统计特征,不是每次结果的绝对误差上限。
基本操作示例
PFADD learn:hll:event player-1 player-2 player-1
PFCOUNT learn:hll:event这是近似去重统计,重复加入同一成员不会按普通计数器累加。示例小集合通常得到 2,但业务不能据此认为任意规模都精确。
游戏与后端场景
适合统计活动参与人数、网站 UV、广告点击用户、日志中的不同 IP。按活动、日期或区域设计键,例如 event:halloween:participants。需要合并多个分组时,可使用 PFMERGE 或相应多键计数,并确认集群同槽条件。
2. 命令速查与示例
按用途分组,共 5 项,覆盖 Redis 8.2.10 的本类型命令及子命令。点击命令名查看官方参数。示例在 redis-cli 中逐行输入,每次换例子前重置下面的教学数据。
准备与重置示例数据
在独立实验实例的 redis-cli 交互模式逐行输入,避免 shell 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。
UNLINK learn:{hll}:a learn:{hll}:b learn:{hll}:out
PFADD learn:{hll}:a player-1 player-2
PFADD learn:{hll}:b player-2 player-3示例的返回说明以刚完成准备段为前提,随机结果与剩余 TTL 不固定。带同一 {...} 的教学键便于在 Cluster 中满足相关多键操作的同槽要求。
基数统计
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| PFADD 加入统计对象 | PFADD learn:{hll}:a player-3返回寄存器是否变化,不代表精确新增人数。 | 每个输入元素 O(1) |
| PFCOUNT 估计不同对象数 | PFCOUNT learn:{hll}:a准备数据的小集合通常估计为 2,但总体是近似结果。 | 单键 O(1) 多键 O(K) |
| PFMERGE 合并多个 HLL | PFMERGE learn:{hll}:out learn:{hll}:a learn:{hll}:b返回 OK,out 表示并集估计,不能直接把人数相加。 | O(K) |
内部诊断命令
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| PFDEBUG 内部 HLL 诊断入口 | PFDEBUG ENCODING learn:{hll}:a内部诊断命令,本例查看 sparse/dense;不是稳定业务接口。 | 未定义 |
| PFSELFTEST 内部 HLL 自测 | PFSELFTEST内部自测可能占用明显 CPU,仅在隔离实验实例按需使用。 | 未定义 |
版本与复杂度补充
下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。
| 命令 | 引入版本与状态 | 复杂度说明 |
|---|---|---|
| PFADD | 2.8.9 | 每个输入元素 O(1),K 个元素为 O(K)。 |
| PFCOUNT | 2.8.9 | 单键平均 O(1);K 个键合并为 O(K),常数较大。 |
| PFMERGE | 2.8.9 | O(K),K 为输入 HLL 数量,合并常数较大。 |
| PFDEBUG | 2.8.9;内部系统命令 | 官方标为 N/A;内部诊断不提供稳定业务性能承诺。 |
| PFSELFTEST | 2.8.9;内部系统命令 | 官方标为 N/A;内部诊断不提供稳定业务性能承诺。 |
通用键操作见键与过期时间,这里集中列出类型自身的命令。
3. 底层编码与内存
逻辑 HLL 在 Redis 中存为 String,内部有 sparse/dense 表示;OBJECT ENCODING 看到的是字符串编码,不直接显示 HLL 稀疏/稠密模式。密集寄存器有效载荷约 12 KiB,实际键占用还包括头部和管理开销。
8.2.10 的 hll-sparse-max-bytes 默认 3000,控制稀疏表示字节预算。超过预算或稀疏格式无法表示时会转为密集形式;不是到固定参与人数就必然转换。
编码与内存怎么测
在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:
PFADD learn:hll:event player-1 player-2
OBJECT ENCODING learn:hll:event
MEMORY USAGE learn:hll:event固定键名、元素字节长度、Redis 版本和分配器,分别记录 1、10、100 个元素或不同值长度时的内存,再跨越编码阈值观察。单元素的增量开销可用相邻两次内存差估计,编码转换时不能套用线性平均;String/Bitmap 的横轴应使用字节数或最高位偏移。比较“一键多元素”和“多个独立键”时,把键名、对象、字典项和 TTL 管理开销一起计入。
MEMORY USAGE 默认对嵌套值采样,SAMPLES 0 会遍历所有嵌套值,仅在小型隔离实验中使用。其结果不等同于进程 RSS;大量键还会增加全局字典、分配器碎片和复制缓冲等成本,需结合 INFO memory。没有适用于所有版本的固定“每 key 几字节”。
依据:8.2.10 配置、对应类型实现、MEMORY USAGE。
4. 性能与阻塞风险
PFCOUNT 单键可复用基数缓存,更新后重算与多个 HLL 合并有不同成本。很多键一次 PFCOUNT/PFMERGE 会占用执行时间。HLL 不保存可遍历成员,没有对应 SCAN;若需要查参与者,必须另存精确集合或权威记录。
常规命令执行主要占用主执行路径,耗时计算会拖慢同实例其他请求;网络 I/O、后台持久化和释放内存有其他线程/进程参与。命令复杂度不包含所有网络、分配、编码转换与客户端成本。
5. 原子性与并发
单次 PFADD/PFMERGE 原子,但多次独立 PFCOUNT 后在应用求和不等于并集人数。活动结束的最终统计需要冻结输入或明确截止水位;使用 Lua 也不能消除概率误差。PFADD 的返回值不能决定是否首次发奖。
MULTI/EXEC 保证排队命令连续执行,但执行时单条报错不会回滚其他已成功命令;WATCH 是乐观冲突检测,需要有界重试。Lua 也不会自动回滚运行错误前的写入,应先校验参数与类型,再修改状态。
6. 过期、持久化与生命周期
TTL 作用于整个 HLL,不能按成员失效或删除成员。按日/活动分键并保留原始桶,滑动时间窗口对有效桶做并集估计;无法从总 HLL 中减去某一天。PFMERGE 目标属于新统计产物,应显式设计其保留期。
大键会增加 RDB/AOF 重写、复制全量同步和恢复时的处理量,fork 写时复制还可能增加峰值内存。单次同步删除聚合对象可能阻塞;UNLINK 先摘除键,把主要回收工作移到后台,但总回收工作和内存压力仍存在,不能替代限长和拆分。
完成教学实验后,只清理自己创建的键,例如 UNLINK learn:hll:event。
7. 集群与扩展
多键 PFCOUNT/PFMERGE 要求同槽。按活动分 tag 方便合并,也把活动热点集中到一个槽;分槽桶不能直接执行跨槽 PFMERGE,需要重新设计统计流水或汇总方式。副本延迟与 HLL 统计误差是两种不同误差来源。
一个键只在一个分片中,hash tag 控制相关键进入同一槽,不会把一个大键切开。副本读取通常是最终一致观察;操作后立即读到本次写入的需求应选主节点或明确的协调策略。
8. Go 客户端与最佳实践
下面函数使用 github.com/redis/go-redis/v9,调用方提供 context.Context 和已配置认证、超时的 *redis.Client。
func HyperLogLogExample(ctx context.Context, rdb *redis.Client, uid string) (int64, error) {
if err := rdb.PFAdd(ctx, "learn:go:event", uid).Err(); err != nil {
return 0, err
}
return rdb.PFCount(ctx, "learn:go:event").Result()
}PFCount 在 Go 中返回 int64,但“整数返回值”不代表精确统计。所有生产者必须使用相同 uid 规范,避免不同字符串表达同一用户。Pipeline 适合不同活动的独立更新;同一活动有多个批次时需明确何时读取最终结果。
普通 Pipeline 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。
继续阅读
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践