亿级数据下的基数统计:从 Set 到 HyperLogLog 的进阶实战

亿级数据下的基数统计:从 Set 到 HyperLogLog 的进阶实战

在移动互联网的业务场景中,我们经常面临"海量数据"统计的挑战。无论是统计 APP 的日活/月活页面的独立访客(UV)搜索词条数 还是 注册 IP 数 ,其核心都是一个典型的数学问题:基数统计(Cardinality Counting),即统计一个集合中不重复元素的个数。

当用户量达到千万甚至亿级时,如何在高并发、高吞吐的场景下,既保证性能又节省内存?本文将带你从最原始的方案开始,逐步引出 Redis 中的王者方案------HyperLogLog


一、 基数统计方案的演进

1. Set 方案:最直观的去重

Redis 的 Set 集合天然去重。每当一个用户访问时,直接将用户 ID 丢进 Set。

  • 操作SADD page_uv:20260209 user123
  • 统计SCARD page_uv:20260209
  • 瓶颈 :如果一个页面有 1000 万用户访问,假设 ID 是 64 位的长整型(8 字节),单个页面仅 ID 存储就要占用约 80MB 内存。如果有 1000 个这样的页面,内存直接爆掉。

2. Hash 方案:灵活但笨重

利用 Hash 结构,将用户 ID 作为 Field,Value 设为 1。

  • 操作HSET page_uv:hash user123 1
  • 统计HLEN page_uv:hash
  • 瓶颈:Hash 的内存消耗比 Set 还要大,因为除了存储元素本身,还需要额外的指针和哈希表结构开销。

3. Bitmap 方案:极致的位运算

Bitmap 像是一个以 Bit 为单位的数组。将用户 ID 映射为 Offset,如果是 1 则代表已访问。

  • 优点 :极省空间。1 亿个 Bit 只需要约 12.5MB
  • 缺点
  1. 偏移量限制:用户 ID 必须是整型且相对密集。如果 ID 离散(如 UUID),需要先转换成数字映射。
  2. 仍有上限:当页面极多时,每个页面维护一个千万级的 Bit 数组,累加起来的内存开销依然不可小觑。

二、 走进 HyperLogLog:用 12KB 解决亿级统计

HyperLogLog(简称 HLL)是一种概率性数据结构 。它的精髓在于:不保存原始数据,只记录特征

核心特性

  • 占用空间极小 :无论你存 10 个还是 1 亿个元素,每个键只需要花费 12KB 内存。
  • 支持超大基数:可以计算接近 个不同元素的基数。
  • 存在误差 :这是一个估算算法,标准误差约为 0.81%。对于 UV、日活这种对绝对精度要求没那么高(99% 准确度即可)的业务,它是完美选择。

Redis 命令行体验

  1. PFADD:添加元素
bash 复制代码
PFADD course:redis:uv user1 user2 user3
  1. PFCOUNT:统计基数
bash 复制代码
PFCOUNT course:redis:uv  # 返回 3
  1. PFMERGE:合并统计(如:统计两个页面的联合 UV)
bash 复制代码
PFADD page_a user1 user2
PFADD page_b user2 user3
PFMERGE page_combined page_a page_b
PFCOUNT page_combined  # 返回 3,user2 被去重

三、 深入浅出:HyperLogLog 的数学原理

HyperLogLog 的基础是 伯努利过程(Bernoulli process)

  1. 抛硬币实验:如果你一直在抛硬币,直到出现正面。如果抛了 次才出现第一个正面,那么我们可以粗略估算,你一共进行了 次实验。
  2. 比特位映射:HLL 将每个元素通过 Hash 函数转为 64 位的二进制串。从低位开始找第一个 "1" 出现的位置。
  3. 分桶平均:为了减少一次偶然性带来的巨大误差,Redis 将 12KB 空间分为 个桶(Buckets)。
  • 每个桶记录该组内观测到的最大 值(即第一个 "1" 出现的位置)。
  • 最终通过 调和平均数(Harmonic Mean) 来计算整体基数,有效修正了异常值的影响。

为什么是 12KB?

个桶即 。每个桶存储的最大 值不超过 64,只需 6 个 bit 即可表示()。


四、 Java 实战:Redisson 整合应用

在 Java 开发中,推荐使用 Redisson ,它封装了极其简便的 RHyperLogLog 对象。

1. Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.7</version>
</dependency>

2. 核心服务类实现

java 复制代码
@Service
public class HyperLogLogService {
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 添加访问记录
     */
    public <T> void add(String logName, T item) {
        RHyperLogLog<T> hll = redissonClient.getHyperLogLog(logName);
        hll.add(item);
    }

    /**
     * 合并多个 Log
     */
    public <T> void merge(String destLogName, String... sourceLogNames) {
        RHyperLogLog<T> hll = redissonClient.getHyperLogLog(destLogName);
        hll.mergeWith(sourceLogNames);
    }

    /**
     * 获取统计基数
     */
    public long count(String logName) {
        RHyperLogLog<Object> hll = redissonClient.getHyperLogLog(logName);
        return hll.count();
    }
}

3. 单元测试演示

java 复制代码
@Test
public void testUVStatistics() {
    String page1 = "uv:page:1";
    String page2 = "uv:page:2";
    
    // 模拟 Page1 访问
    hyperLogLogService.add(page1, "user_A");
    hyperLogLogService.add(page1, "user_B");
    
    // 模拟 Page2 访问
    hyperLogLogService.add(page2, "user_B"); // 重复用户
    hyperLogLogService.add(page2, "user_C");
    
    // 合并统计
    String totalKey = "uv:total";
    hyperLogLogService.merge(totalKey, page1, page2);
    
    System.out.println("合并后的总 UV: " + hyperLogLogService.count(totalKey)); // 输出 3
}

五、 总结与对比

维度 Set Bitmap HyperLogLog
准确性 100% 准确 100% 准确 约 0.81% 误差
内存消耗 随数据量线性增长 (极大) 随 Offset 范围增长 (中) 固定 12KB (极小)
存储内容 存储原始值 存储位状态 不存储原始值
适用场景 需要取出原始数据去重 二值状态统计(签到、状态位) 海量基数统计(UV、日活)

核心建议

在进行海量数据统计时,如果业务对误差不敏感(如向公众展示的阅读量、在线人数),HyperLogLog 是毫无疑问的最优解。它用极小的内存代价,解决了分布式环境下最棘手的统计问题。

相关推荐
程序猿ZhangSir5 分钟前
详解了解 Redis IO多路复用底层原理,Select,poll,epoll三者的区别?
数据库·redis·缓存
编程之升级打怪34 分钟前
用Python语言实现简单的Redis缓冲数据库驱动库
redis·python
SadSunset39 分钟前
第一章:Redis 入门介绍
数据库·redis·缓存
Arthas2174 小时前
互联网大厂Java面试实录:谢飞机的电商微服务之旅 - Spring Boot/Cloud/Redis/Kafka实战
spring boot·redis·spring cloud·微服务·kafka·java面试·电商
IAtlantiscsdn4 小时前
Redis面试题总结
数据库·redis·缓存
Mr.wangh7 小时前
redis面试题总结
java·redis·面试
未秃头的程序猿8 小时前
Redis也能做消息队列!Spring Boot实战:从List到Stream的优雅实现
redis·后端
SadSunset8 小时前
第五章:Redis 的 Java 客户端
java·数据库·redis
程序员阿伦8 小时前
谢飞机面Java大厂:音视频场景下的Spring Boot + Kafka + Redis实战三连问
spring boot·redis·kafka·java面试·音视频架构·微服务容错