SADD统计UV最直接因自动去重且O(1)时间复杂度;适用单日/稳定ID场景,禁用INCR/HSET误计;key须按时间窗口和业务维度设计并配EXPIRE;获取UV仅用SCARD,禁用SMEMBERS或KEYS;跨周期合并应预建周期key或离线计算。

为什么用 SADD 统计 UV 最直接
因为每个用户 ID 写入 SET 时,SADD 自动忽略重复值,底层哈希表保证 O(1) 去重 —— 不需要你写 if 判断是否存在,也不依赖外部逻辑兜底。
常见错误现象:INCR 或 HSET 误用:有人用 INCR uv:20240601 统计,结果把 PV 当 UV;或者用 HSET 存用户 ID + 时间戳,却忘了查重,导致重复计数。
- 适用场景:单日 UV、按设备 ID / 登录 ID / 匿名 token 统计(注意 ID 必须稳定且有业务意义)
- 不适用场景:需要实时精确到秒级去重的长周期 UV(如 30 天去重),
SET占内存高,建议用 HyperLogLog - 性能影响:1000 万用户 ID 的
SET约占 200–300MB 内存,比PFADD高 5–10 倍,但精度 100%
SADD 的 key 设计决定统计粒度
key 名不是随便拼的,它直接绑定时间窗口和业务维度。比如 uv:day:20240601 和 uv:hour:2024060114 是两个完全独立的集合,互不影响。
容易踩的坑:uv:user_id 这种 key 意味着全量用户塞进一个 set,无法分片、无法过期、无法清理 —— 一旦写错,内存只增不减。
- 推荐格式:
uv:{time_window}:{scope},例如uv:day:20240601:web(Web 端单日 UV) - 必须配
EXPIRE:写完SADD紧跟EXPIRE uv:day:20240601 86400,否则 key 永久存在 - 避免中文或空格:
uv:日活:20240601会导致部分客户端解析异常,一律用英文+数字+下划线
如何安全地获取当日 UV 数量
SCARD 是唯一正确方式。别用 SMEMBERS 拉全量再 count,数据一过万就卡顿甚至触发慢日志。
常见错误现象:用 KEYS uv:day:* 扫所有日期 key 再逐个 SCARD —— 这在生产环境属于高危操作,阻塞主线程。
- 正确做法:应用层维护日期变量,明确调用
SCARD uv:day:20240601 - 批量查询多个日期?用 pipeline 封装多个
SCARD,不要用KEYS或SCAN做元数据扫描 - 注意返回值类型:
SCARD返回整数,若 key 不存在返回0,不是nil—— 别在代码里判nil导致逻辑跳过
跨天/跨周期 UV 合并的现实约束
Redis 原生命令不支持两个 SET 取并集后直接返回基数,SUNIONSTORE 会写新 key,SUNION 会把全部元素传回客户端 —— 100 万用户 ID 就是几 MB payload,网络和内存双开销。
真正能落地的方案只有两种:要么提前规划好周期 key(如用 uv:week:2024W23 单独维护),要么导出到离线系统用 Spark/Hive 计算。
- 别在 Redis 里做
SUNION+SCARD组合:一次请求可能耗时数百毫秒,且不可缓存 - 如果必须运行时合并,用
SUNIONSTORE tmp:uv:merge uv:day:20240601 uv:day:20240602,再SCARD,最后DEL tmp:uv:merge—— 但要注意并发写冲突 - 更稳的做法:每天凌晨用 Lua 脚本原子化地把昨日 key merge 到周 key 中,避免白天查时临时计算
UV 统计看着简单,难点从来不在加法,而在 key 生命周期管理、内存水位预估、以及跨周期合并时对 Redis 特性的误判 —— 很多人卡在“以为 SADD 写进去就完了”,其实后面三步才是真功夫。