HLL专为UV等基数估计设计,内存恒定(仅12KB)、误差可控(0.81%),而SET存储亿级ID将耗几十GB;PFADD是写入命令,PFCOUNT才返回估算值,二者不可混淆。

为什么直接用 HLL 而不是 SET 或 PFADD?
因为你要统计的是「全局唯一用户行为次数」,比如每日 UV、接口调用去重计数,而不是存原始 ID。用 SET 会撑爆内存(亿级 ID ≈ 几十 GB),而 PFADD 是 HLL 的写入命令,本身不返回结果——你真正需要的是 PFCOUNT 的估算值,且必须接受 0.81% 左右误差。Go 客户端里别误把 PFADD 当成原子计数函数用,它只负责“添加”,不保证幂等性(重复 ID 多次 PFADD 仍只算 1 次,这点没问题,但得确认业务是否允许估算)。
Go 里用 redis.Client 调用 PFCOUNT 的正确姿势
别用 Do() 手拼命令,也别自己封装 PFADD + PFCOUNT 两步——HLL 天然支持单 key 多次 PFADD 后一次 PFCOUNT,所以高频写场景下应批量合并写入,再定时读取。关键点:
-
PFADD返回int64:1 表示该 key 的基数发生了变化(即新元素加入),0 表示无变化;不要拿这个当“是否成功”的判断依据,网络失败时会报错 -
PFCOUNT返回int64,就是估算的唯一值数量,注意它可能为 0(key 不存在或空HLL) - Go redis 客户端(如
github.com/go-redis/redis/v9)里对应方法是client.PFAdd(ctx, key, values...interface{})和client.PFCount(ctx, keys...string) - 如果要跨天统计(如「近 7 天 UV」),别用单个 key 存所有数据,而是按天分 key:
uv:20240501、uv:20240502,再用PFMERGE合并后PFCOUNT——但注意PFMERGE会新建 key,原 key 不变
并发写入时 PFADD 是否线程安全?
Redis 本身是单线程,PFADD 命令天然原子,不需要加锁。但 Go 微服务常有多个 goroutine 同时写同一个 HLL key,这时候要注意:
- 别在循环里反复创建
context.WithTimeout,共用一个带超时的ctx即可 - 如果用连接池(默认开启),
go-redis会自动复用连接,无需额外处理 - 极端高并发下(QPS > 5k),单个
HLLkey 可能成为 Redis 热点,考虑按业务维度分片,例如:uv:login:{shard},其中shard = userID % 16,最后用PFMERGE汇总 - 别在
defer里调用PFCOUNT——延迟执行可能读到旧值,需同步获取
怎么验证 HLL 结果没崩?
线上出问题往往不是算法不准,而是 key 写错、TTL 过期、或误用了 INCR 类命令覆盖了 HLL 结构。排查时优先检查:
- 用
redis-cli执行TYPE your:hll:key,确认返回hyperloglog,不是string或none - 用
EXISTS your:hll:key看 key 是否存在;若不存在,说明PFADD根本没写进去(可能是 ctx 超时或网络断开) - 用
PFCOUNT your:hll:key直接查 Redis,对比 Go 代码输出,排除客户端缓存或序列化干扰 - 注意 TTL:如果设置了过期时间,
HLL过期后PFCOUNT返回 0,且不会自动重建——业务侧得自己判断是否需初始化
误差和分片逻辑是可控的,真正容易漏掉的是 key 生命周期管理和类型校验。


















