设计一个网站的UV(独立访客)统计,每天千万级访问量,但不要求100%精确,用Redis的什么数据结构?
针对每天千万级访问量,且不要求 100% 精确的 UV(独立访客)统计需求,Redis 最完美的解决方案是使用 HyperLogLog 数据结构。
下面是详细的分析和设计方案:
1. 为什么选择 HyperLogLog?
在 Redis 中,统计去重数据通常有三种选择:Set、Bitmap 和 HyperLogLog。我们来做个对比:
- Set(集合): 可以做到 100% 准确。但是极其消耗内存。如果一个用户 ID 是 32 字节的字符串,千万级 UV 一天需要占用约 300MB - 500MB 的内存。一个月就是十几 GB,成本太高。
- Bitmap(位图): 内存占用小(一千万连续用户 ID 约占用 1.2MB)。但前提是用户 ID 必须是连续的数字。如果业务用的是 UUID、设备指纹或非连续 ID,Bitmap 就会变得极其稀疏,导致内存浪费,且需要维护 ID 映射表,极其繁琐。
- HyperLogLog(基数统计):
- 极限省内存: 无论统计多少个用户(最多支持 个),每个 Key 最大只占用 12 KB 的内存。
- 满足误差允许条件: 它是概率算法,标准误差率在 0.81% 左右。对于千万级 UV,误差在几万左右,在宏观数据统计上完全可以接受。
- 支持任意格式的 ID: 无论是字符串、UUID 还是数字,直接塞进去就行。
2. 具体实现方案
2.1 Key 的设计
通常我们需要按天统计,甚至可能需要区分不同平台(如 PC、App、小程序)。
Key 可以设计为:uv:{业务线}:{平台}:{YYYYMMDD}
例如:uv:homepage:web:20231024
2.2 核心操作命令
① 记录用户访问(每次用户访问时调用)
使用 PFADD 命令将用户的唯一标识(User ID / Device ID / IP)加入到当天的 Key 中。
bash
# 用户 user1001 访问
PFADD uv:homepage:web:20231024 "user1001"
(注:PFADD 的时间复杂度是 O(1),执行极快。)
② 获取当天的 UV 数(后台看板查询时调用)
使用 PFCOUNT 命令获取估算值。
bash
PFCOUNT uv:homepage:web:20231024
# 返回:10543201
③ 统计周 UV / 月 UV(高级玩法)
UV 统计不能简单地将每天的 UV 数量相加,因为同一个用户可能周一和周二都访问了。HyperLogLog 提供了 PFMERGE 命令,可以将多个天的 HLL 合并成一个新的 HLL,自动去重!
bash
# 将周一到周日的 UV 合并到一个周统计的 Key 中
PFMERGE uv:homepage:web:week_42 uv:homepage:web:20231016 uv:homepage:web:20231017 ... uv:homepage:web:20231022
# 然后计算周 UV
PFCOUNT uv:homepage:web:week_42
3. 高并发生产环境优化建议
虽然 Redis 的 PFADD 很快,但在每天千万级、瞬时并发极高的情况下,每个请求都去打一次 Redis 依然会浪费网络 I/O。建议采取以下优化:
- 本地缓存/批量写入(Batching):
在应用层(如 Java/Go 程序)维护一个本地的 Set 或内存队列,攒够一定数量的用户 ID(比如 1000 个)或者每隔几秒钟,使用一次PFADD将一批数据打包发给 Redis,极大降低 Redis 的 QPS 压力。bash# 一次性添加多个用户 PFADD uv:homepage:web:20231024 "user1" "user2" "user3" ... - 设置过期时间(TTL):
按天统计的日历数据如果长期不看,没必要永远保留在内存里。建议每天的 Key 写入后设置过期时间(如EXPIRE key 2592000保留 30 天)。历史数据可以通过定时任务异步落盘到 MySQL 或数仓(ClickHouse/Hive)中持久化。 - 读写分离:
如果管理后台实时查看大盘数据的频率很高,可以在从节点(Slave)执行PFCOUNT,避免占用主节点(Master)的处理资源。
总结
对于千万级 + 允许轻微误差的 UV 统计,HyperLogLog 是量身定制的银弹。它用极其微小(12KB)的内存代价,换取了极高的写入和统计性能。
右滑查看面试常问