Redis超省内存的UV统计方案
1.UV统计
UV(Unique Visitor,独立访客)统计是互联网产品中最核心的数据指标之一。
简单来说,UV 统计的就是“有多少个不同的人访问了你的网站或功能”。
为了让你更好地理解,我们通常会把它和PV放在一起对比:
PV (Page View,页面浏览量):不管是谁,只要页面被打开/刷新了一次,PV 就 +1。如果我今天手贱按了 100 次 F5 刷新你的主页,那你的 PV 就增加了 100。
UV (Unique Visitor,独立访客):基于用户的唯一标识(比如用户 ID、IP 地址或设备号)来去重。哪怕我今天疯狂刷新了 100 次你的主页,对不起,我只算 1 个 UV。
在后端开发中,UV 统计为什么是个“难题”?
既然 UV 的核心是去重,那后端程序就必须“记住”今天到底有哪些用户来过。 如果你的网站日活只有几百人,你可以直接把来过的 UserID 塞进 Java 的HashSet或者 Redis 的Set里,通过集合天然的不可重复性来得出 UV。
但是,如果你的产品做大了(比如百万级日活): 把 100 万个用户的长整型 ID 全存进 Redis 的Set里,单单为了统计今天这一个页面的 UV,可能就要耗费几十上百 MB 的内存!一个月就是几个 G!如果有 10 个页面要统计呢?内存直接被撑爆了。
2.HyperLogLog (HLL)
为了解决这种海量数据的去重统计问题,Redis 提供了一个极其神奇的数据结构,叫做HyperLogLog (简称 HLL),它天生就是用来做 UV 统计的。
它的核心特点是:
极致省内存:无论你放 1 万个用户,还是 1 个亿的用户 ID 进去,它在 Redis 里最多只占用 12 KB 的内存!
存在极小误差:它是基于一种概率算法估算出来的,标准误差率在
0.81%左右。对于 UV 这种宏观统计数据(比如报表上显示 100.2 万人访问和 100.4 万人访问),运营是根本不在乎这千分之八的误差的。
Redis HyperLogLog 的 API 设计非常极简,它的核心命令只有 3 个(所有命令都以PF开头,是为了纪念这种数据结构算法的发明者 Philippe Flajolet)。
1. Redis 命令
PFADD key element [element ...](添加元素)将一个或多个用户 ID 存入对应的 key 中。
执行逻辑:如果这是该用户今天第一次访问,Redis 内部的基数估算发生了变化,会返回
1;如果用户今天已经访问过(已被记录),则返回0。# 添加今天的 UV 记录 PFADD uv:2026-07-30 user_101 user_102
PFCOUNT key [key ...](统计数量)获取一个或多个 key 的近似唯一总数(误差率约为 0.81%)。
# 查看今天有多少独立访客 PFCOUNT uv:2026-07-30
PFMERGE destkey sourcekey [sourcekey ...](合并统计)将多个 HyperLogLog 合并为一个新的 HyperLogLog。
核心价值:极度适用于统计周活跃用户 (WAU)或月活跃用户 (MAU)。因为你不能简单地把每天的 UV 数量相加(同一个用户可能周一和周二都访问了,直接相加算作 2 个人,但用
PFMERGE合并后,底层算法会自动去重,依然算作 1 个人)。# 将周一到周日的 UV 数据,合并到一个"本周总UV"的 key 中 PFMERGE uv:week_31 uv:2026-07-27 uv:2026-07-28 ...
2.Spring Boot
在 Java 后端工程中,通常通过 Spring Data Redis 提供的StringRedisTemplate来操作 HyperLogLog
public void hyperLogLogOperationsDemo() { String key1 = "uv:20260728"; String key2 = "uv:20260729"; String destKey = "uv:both_days"; // 1. PFADD (添加元素) // 对应命令: PFADD key element [element ...] stringRedisTemplate.opsForHyperLogLog().add(key1, "user1001", "user1002"); // 用户1001和1002在28日活跃 stringRedisTemplate.opsForHyperLogLog().add(key2, "user1001", "user1003"); // 用户1001和1003在29日活跃 // 2. PFCOUNT (获取近似基数值) // 对应命令: PFCOUNT key [key ...] Long count1 = stringRedisTemplate.opsForHyperLogLog().size(key1); Long count2 = stringRedisTemplate.opsForHyperLogLog().size(key2); System.out.println(" 28日UV数: " + count1); System.out.println(" 29日UV数: " + count2); // 输出示例: // 28日UV数: 2 // 29日UV数: 2 // 3. PFMERGE (合并多个HyperLogLog) // 对应命令: PFMERGE destkey sourcekey [sourcekey ...] stringRedisTemplate.opsForHyperLogLog().union(destKey, key1, key2); // 再次使用 PFCOUNT 获取合并后的总人数 Long mergedCount = stringRedisTemplate.opsForHyperLogLog().size(destKey); System.out.println("\n3. PFMERGE合并后的活跃总人数: " + mergedCount); // 输出示例: 3. PFMERGE合并后的活跃总人数: 3 (user1001在底层被算法自动去重) }