亿级数据下的基数统计:从 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 是毫无疑问的最优解。它用极小的内存代价,解决了分布式环境下最棘手的统计问题。

相关推荐
学到头秃的suhian16 小时前
Redis缓存
数据库·redis·缓存
苏渡苇16 小时前
Java + Redis + MySQL:工业时序数据缓存与持久化实战(适配高频采集场景)
java·spring boot·redis·后端·spring·缓存·架构
mqffc20 小时前
spring session、spring security和redis整合的简单使用
redis·spring·bootstrap
indexsunny21 小时前
互联网大厂Java面试实战:Spring Boot到Kafka的技术问答解析
java·spring boot·redis·junit·kafka·spring security·microservices
流氓也是种气质 _Cookie21 小时前
Linux上安装Docker
linux·redis·docker
茶杯梦轩1 天前
从零起步学习Redis || 第十章:主从复制的实现流程与常见问题处理方案深层解析
服务器·redis
Wzx1980121 天前
高并发秒杀下,如何避免 Redis 分布式锁的坑?
数据库·redis·分布式
小毅&Nora1 天前
【后端】【Redis】④ Redis 7/8 TopK 新特性:从“热搜榜”到“实时风控”,一文彻底掌握高频元素统计神器
redis·缓存·bloom
brucelee1861 天前
创建AWS ElastiCache Redis
redis·云计算·aws
独自破碎E1 天前
怎么知道本地的Redis有没有设置密码
数据库·redis·缓存