HLL适合统计每日UV因内存仅约12KB且误差率约0.81%,远优于SET;需按天用UTC时间构造key隔离数据,PFADD并发安全但禁在事务中重复调用,PFCOUNT仅为估算值不可对账。

为什么 HLL 适合统计每日 UV,而不是用 SET
因为 SET 存每个用户 ID 会吃大量内存(比如千万级 UV 就要千万个字符串),而 HyperLogLog 固定只用约 12 KB 内存,误差率控制在 0.81% 左右——对 UV 这类“大概准”的指标完全够用。Golang 客户端(如 github.com/go-redis/redis/v9)对 HLL 操作封装得很干净,但要注意:它不返回真实基数,只提供 PFCOUNT 的估算值。
PfAdd 必须带日期前缀,否则数据会混在一起
Redis 的 HLL 是 key 级存储,同一个 key 多次 PfAdd 是累加去重,但不同天的数据必须隔离。常见错误是直接用 hll:uv 当 key,结果所有日期的 UV 全被算进去了。
正确做法是按天构造 key:
key := fmt.Sprintf("hll:uv:%s", time.Now().UTC().Format("2006-01-02"))
- 用 UTC 时间避免时区错乱(尤其服务部署在多区域时)
- 不要用本地时间或
time.Date手动拼接,容易漏掉时区偏移 - 如果业务需要支持指定日期(比如补录昨日数据),记得传入对应
time.Time并格式化
并发写入 PfAdd 安全,但别在事务里反复调用
PfAdd 本身是原子操作,多个 goroutine 同时往同一个 HLL key 写用户 ID 不会出错。但注意两个坑:
立即学习“go语言免费学习笔记(深入)”;
- 不要在一个
pipeline或transaction中对同一 key 多次PfAdd——Redis 不会报错,但实际只生效最后一次(因为PfAdd返回值是“是否新增了元素”,多次调用后内部结构可能被覆盖) - 批量写入推荐用
PFADD key element1 element2 ...一次提交多个 ID,比循环单次调用快且省连接 - Golang 客户端调用示例:
client.PFAdd(ctx, key, userID).Result(),userID类型必须是interface{},通常传string或int64都可以,但要保持一致(混用会导致逻辑重复计数)
读取 UV 时别依赖 PFCOUNT 实时精确值
PFCOUNT 返回的是估算值,且 Redis 内部有缓存机制:当 HLL 结构未变更时,连续调用可能返回相同结果,哪怕刚 PfAdd 了新元素。这不是 bug,是设计使然。
- 日常监控可直接用
client.PFCount(ctx, key).Val() - 如果要做“当日 UV 达到阈值触发告警”,建议加一层本地缓存(比如每分钟刷新一次并存到内存 map),避免高频查 Redis 导致误判
- 千万别拿
PFCOUNT和数据库里COUNT(DISTINCT user_id)对账——两者原理不同,数值必然不等,对账只会引发困惑
真正难处理的是跨日聚合(比如“最近 7 天独立访客”),这得用 PFMERGE,但它不支持差集或交集,业务上得提前规划好 key 命名和生命周期,不然 merge 出来的结果没法解释。


















