三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Redis超省内存的UV统计方案

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 万个用户,还是 1 个亿的用户 ID 进去,它在 Redis 里最多只占用 12 KB 的内存

  2. 存在极小误差:它是基于一种概率算法估算出来的,标准误差率在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在底层被算法自动去重) }
← 返回列表