Redis HyperLogLog 实现 UV 统计:为什么 100 万用户也能用不到 16KB

本文整理自黑马点评 Redis 实战篇第 12 章「UV 统计」。这一章很短,但很重要:它讲的是 Redis HyperLogLog,适合解决海量去重计数问题,比如统计一个页面每天有多少独立访客。

1. 这篇文章解决什么问题

网站或 App 经常需要统计访问量。

常见指标有两个:

PV:页面访问次数
UV:独立访客数

PV 很好统计,每访问一次加 1 即可。

UV 麻烦一些,因为它要去重:

同一个用户一天访问 10 次,只能算 1 个 UV。

如果访问量很大,怎么在 Redis 中低成本统计 UV?这就是 HyperLogLog 的应用场景。


2. PV 和 UV 的区别

PV:Page View

PV 表示页面访问量。

用户每访问一次页面,就算一次。

例如:

用户 A 访问 5 次
用户 B 访问 2 次
用户 C 访问 1 次

PV 是:

5 + 2 + 1 = 8

UV:Unique Visitor

UV 表示独立访客数。

同一个用户一天内访问多次,只算一次。

上面的例子中,UV 是:

A, B, C 三个不同用户 = 3

所以:

PV 统计访问次数
UV 统计去重用户数

3. 为什么 UV 统计难

如果只是统计 PV,可以用 Redis 自增:

INCR pv:home:20260511

但 UV 需要去重,必须知道某个用户今天是否已经统计过。

最直接的方法是用 Set:

SADD uv:home:20260511 user_1
SADD uv:home:20260511 user_2
SADD uv:home:20260511 user_1
SCARD uv:home:20260511

Set 的好处是精确。

但问题是:它要保存每个用户标识。

如果一天有 1000 万 UV,Set 里就要存 1000 万个用户 id,内存开销很大。

这时就可以考虑 HyperLogLog。


4. HyperLogLog 是什么

HyperLogLog,简称 HLL,是一种概率型基数统计算法。

基数就是集合中不重复元素的数量。

例如:

{A, B, C} 的基数是 3
{A, A, B, C} 去重后还是 {A, B, C},基数也是 3

UV 统计本质上就是统计基数。

HLL 的特点是:

不保存所有元素
只估算不重复元素数量
内存极低
结果有小误差

Redis 中单个 HyperLogLog 的内存通常小于 16KB,误差小于约 0.81%。

对于 UV 报表来说,这点误差通常可以接受,因为 UV 更多用于趋势分析,不像订单金额那样必须一分不差。


5. HyperLogLog 是 Redis 新数据类型吗

从使用层看,Redis 提供了专门的 HLL 命令:

PFADD
PFCOUNT
PFMERGE

所以可以把它看成 Redis 的一种特殊统计结构。

但从底层实现看,Redis HLL 也是基于 String 编码的特殊结构。

这点和 BitMap 有点像:

BitMap:底层基于 String,用 SETBIT / GETBIT 操作
HLL:底层也基于 String,用 PFADD / PFCOUNT 操作

所以更准确的说法是:

HyperLogLog 是 Redis 提供的概率型基数统计结构,底层基于 String,但不能当普通字符串用,应该通过 PFADD、PFCOUNT、PFMERGE 操作。


6. 实际业务怎么用 HLL

假设要统计首页每日 UV。

可以设计 key:

uv:home:20260511

用户访问首页时:

String key = "uv:home:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String userId = UserHolder.getUser().getId().toString();
stringRedisTemplate.opsForHyperLogLog().add(key, userId);

对应 Redis:

PFADD uv:home:20260511 5

如果用户 5 今天访问首页 10 次,重复执行 PFADD,HLL 仍然会把它估算为一个独立访客。

后台报表查询 UV:

Long uv = stringRedisTemplate.opsForHyperLogLog().size("uv:home:20260511");

对应 Redis:

PFCOUNT uv:home:20260511

7. Java 里 HLL 对应什么

Spring Data Redis 中,HLL 的入口是:

stringRedisTemplate.opsForHyperLogLog()

常用方法:

add(key, values)   // 对应 PFADD
size(key)          // 对应 PFCOUNT
union(dest, keys)  // 对应 PFMERGE

注意:Java 里不会让你 new HyperLogLog()

HLL 结构存在 Redis 中,Java 只是通过 StringRedisTemplate 发送命令操作它。


8. 测试百万数据

项目测试代码:

@Test
void testHyperLogLog(){
    String [] values = new String[1000];
    int j=0;
    for (int i=0;i<1000000;i++){
        j=i%1000;
        values[j]="user_"+i;
        if (j==999){
            stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
        }
    }
    Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
    System.out.println("count: "+count);
}

这段代码模拟向 HLL 中添加 100 万个不同用户:

user_0
user_1
...
user_999999

理论 UV 是:

1,000,000

但 HLL 返回的是估算值,可能接近 100 万,但不一定刚好等于 100 万。


9. 为什么要用数组批量 add

String [] values = new String[1000];

这里准备了一个长度为 1000 的数组。

循环中:

j = i % 1000;
values[j] = "user_" + i;
if (j == 999) {
    stringRedisTemplate.opsForHyperLogLog().add("hl2", values);
}

意思是每攒够 1000 个用户,就批量写一次 Redis。

如果一个一个写:

100 万次 Redis 请求

批量写后:

100 万 / 1000 = 1000 次 Redis 请求

网络开销大幅降低。


10. add 和 size 分别对应什么

stringRedisTemplate.opsForHyperLogLog().add("hl2", values);

对应:

PFADD hl2 user_0 user_1 user_2 ...

它的作用是把用户标识加入 HLL 统计结构。

注意,它不是像 Set 那样完整保存每个用户,而是更新内部概率统计结构。

Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");

对应:

PFCOUNT hl2

它返回估算的去重数量。


11. HLL 和 Set 怎么选

用 Set 的场景

需要精确结果
需要判断某个用户是否存在
需要列出所有用户
数据量不大

用 HyperLogLog 的场景

只需要估算数量
数据量巨大
能接受小误差
不需要知道具体有哪些用户

UV 统计一般适合 HLL。

订单、支付、库存这类需要精确结果的业务绝对不适合 HLL。


12. 完整业务流程

用户访问页面

生成 UV key

PFADD key userId

HLL 更新内部统计结构

运营后台查询报表

PFCOUNT key

返回估算 UV

文字版:

用户访问页面
  -> PFADD uv:{page}:{date} userId
  -> HLL 自动去重估算
  -> 报表查询 PFCOUNT
  -> 得到估算 UV

13. 易错点

1. HLL 不是精确统计

它是概率估算,有误差。

2. HLL 不能列出具体用户

它只能告诉你大概有多少个不同用户,不能告诉你具体是谁。

3. HLL 不适合需要强一致的业务

比如订单数、库存数、金额统计都不能用 HLL。

4. hl2 只是测试 key

真实业务应该设计成:

uv:home:20260511
uv:shop:{shopId}:20260511

5. Java 里没有直接 new HLL

通过:

stringRedisTemplate.opsForHyperLogLog()

操作 Redis 中的 HLL。


14. 面试怎么说

如果面试官问:Redis HyperLogLog 适合什么场景?

可以回答:

HyperLogLog 适合海量数据的去重计数,比如 UV、独立 IP 数、搜索关键词去重数量等。它不保存所有元素,而是用概率算法估算基数,单个 HLL 内存很小,Redis 中通常小于 16KB,但结果有小误差,大约小于 0.81%。所以它适合允许误差的统计场景,不适合订单金额、库存这类必须精确的业务。

如果问 Java 里怎么用:

可以通过 stringRedisTemplate.opsForHyperLogLog() 操作。访问发生时用 add(key, userId),对应 PFADD;统计时用 size(key),对应 PFCOUNT


15. 总结

第 12 章的核心是:

UV 统计的本质是海量去重计数。如果用 Set 保存所有用户,结果精确但内存高;HyperLogLog 不保存完整用户集合,而是用概率算法估算基数,用极低内存换取可接受的小误差。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐