个人主页北极的代码欢迎来访作者简介java后端学习者❄️个人专栏苍穹外卖日记SSM框架深入JavaWeb✨命运的结局尽可永在不屈的挑战却不可须臾或缺摘要本文介绍了使用Redis的HyperLogLog数据结构实现高效UV统计的方案。相比传统Set或数据库方案HyperLogLog仅需14KB内存即可统计百万级UV数据误差率仅0.24%。文章详细分析了HyperLogLog的核心原理、内存占用对比、代码实现示例并给出了项目中的架构设计和业务层代码。该方案在保证统计精度的同时内存消耗仅为Set方案的1/1000是典型的空间换时间优化案例适用于对精度要求不高的UV统计场景。一、背景什么是UV为什么要做UV统计在做网站运营时我们经常需要关注两个指标PVPage View页面访问量。用户每打开一次页面就记录一次多次打开则累加。UVUnique Visitor独立访客量。同一个用户一天内多次访问只记录一次。简单来说UV反映的是一个网站有多少「真实的人」在访问是衡量网站流量的核心指标之一。在黑马点评项目中我们需要统计每个页面的UV。但这里有个难题如何判断一个用户是否已经统计过了传统的做法是把访问过的用户ID都存起来每次访问时判断是否已存在。但如果日活是百万级存储这些用户ID就需要巨大的内存空间。二、方案选型为什么不用Set或数据库方案一数据库去重直接用数据库表存储用户ID和访问记录每次访问先查询再插入。缺点百万级UV数据库压力巨大查询耗时高。方案二Redis Set利用Redis的Set结构存储用户ID自动去重最后用SCARD获取数量。缺点如果100万个用户ID每个存8字节Long类型加上Redis内部开销大约需要15-20MB内存。20MB看起来不大但如果同时统计多个页面、多天的数据内存消耗就会成倍增长。三、终极方案Redis HyperLogLog3.1 什么是HyperLogLogHyperLogLog简称HLL是一种概率性数据结构专门用于计算集合的基数不重复元素的数量。Redis中的HLL基于String结构实现最大的特点是不管统计多少数据单个HLL的内存永远小于16KB![HLL内存占用对比图]作为代价HLL的统计结果是一个近似值误差率约为0.81%。对于UV统计来说这个误差完全可以接受。3.2 HyperLogLog核心原理简化版HLL的核心思想非常巧妙它不存储元素本身而是通过哈希概率估计算法来推算基数。哈希处理对每个元素计算64位的哈希值分桶存储64位哈希中前14位用来决定「桶编号」共16384个桶后50位用来统计「二进制连续0的个数」概率估算通过所有桶中的最大值利用数学公式估算出总的不重复元素数量这种「空间换时间」的思路让HLL在极低内存消耗下依然能保持可接受的统计精度。3.3 HyperLogLog常用命令bash# 添加元素 PFADD key element [element ...] # 统计基数 PFCOUNT key [key ...] # 合并多个HLL PFMERGE destkey sourcekey [sourcekey ...]四、代码实战百万UV统计测试为了验证HyperLogLog的实际效果我们进行一个百万级数据的压力测试。4.1 测试代码javaTest void testHyperLogLog() { // 准备100万条用户数据 String[] values new String[1000]; for (int i 0; i 1000000; i) { int j i % 1000; values[j] user_ i; if (j 999) { // 每凑够1000条批量存入Redis stringRedisTemplate.opsForHyperLogLog().add(uv:test, values); } } // 统计UV数量 Long uvCount stringRedisTemplate.opsForHyperLogLog().size(uv:test); System.out.println(统计结果 uvCount); }4.2 内存占用对比测试bash# 插入数据前查看内存 127.0.0.1:6379 INFO memory # used_memory: 905584 # 插入100万条数据后 127.0.0.1:6379 INFO memory # used_memory: 919968 # 内存增量 919968 - 905584 14384 bytes ≈ 14KB4.3 测试结果分析统计项数值实际插入数据量1,000,000HyperLogLog统计结果997,593误差率0.24%远低于理论的0.81%内存占用约14KBSet方案预估内存约15-20MB内存节省比例约1000倍结论用14KB的内存完成了百万级UV统计误差仅0.24%完全满足业务需求。五、项目中的UV统计设计总结5.1 整体架构设计text用户访问页面 → 后端接收请求 → 提取用户标识userId/IP → PFADD uv:{pageId}:{date} userId → 定时/实时统计 PFCOUNT 获取UV5.2 关键设计要点分页签统计对不同页面分别统计key格式为uv:page:{pageId}:{yyyyMMdd}分时段统计支持按天、按周、按月聚合分析合并查询使用PFMERGE实现多天UV去重统计5.3 业务层代码示例javaService public class UVStatisticsService { Autowired private StringRedisTemplate stringRedisTemplate; /** * 记录一次页面访问用于UV统计 * param pageId 页面标识 * param userId 用户ID */ public void recordUV(String pageId, Long userId) { String date LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String key uv:page: pageId : date; stringRedisTemplate.opsForHyperLogLog().add(key, userId.toString()); } /** * 获取某页面某天的UV */ public Long getUV(String pageId, LocalDate date) { String key uv:page: pageId : date.format(DateTimeFormatter.ofPattern(yyyyMMdd)); return stringRedisTemplate.opsForHyperLogLog().size(key); } /** * 获取某页面最近7天的去重UV周活跃 */ public Long getWeeklyUV(String pageId, LocalDate endDate) { String tempKey uv:temp: UUID.randomUUID(); ListString keys new ArrayList(); for (int i 0; i 7; i) { String date endDate.minusDays(i).format(DateTimeFormatter.ofPattern(yyyyMMdd)); keys.add(uv:page: pageId : date); } // 合并7天的HLL stringRedisTemplate.opsForHyperLogLog().union(tempKey, keys.toArray(new String[0])); Long result stringRedisTemplate.opsForHyperLogLog().size(tempKey); stringRedisTemplate.delete(tempKey); return result; } }六、方案优缺点分析与使用建议优点维度说明内存效率极高100万数据仅14KB性能好写入和查询都是O(1)复杂度支持合并PFMERGE可轻松实现多天去重缺点与注意事项缺点应对策略有0.81%误差对UV统计来说完全可接受无法取出具体元素需要精确用户列表时改用Set小数据量时误差偏大数据量1000时考虑降级方案选型建议做UV统计首选HyperLogLog需要精确统计且数据量小用Set需要知道具体是哪些用户用Set或数据库需要实时精确计数用BitMap如签到场景七、总结黑马点评项目中使用HyperLogLog实现UV统计是一个非常经典的空间换时间的案例传统Set方案百万UV需15-20MB内存HyperLogLog方案百万UV仅需14KB内存节省约1000倍误差仅0.24%这个案例很好地说明了没有最好的方案只有最合适的方案——根据业务场景对精度的要求选择性价比最高的技术实现。结语如果对你又帮助请点赞关注收藏你的支持就是我最大的鼓励
【黑马点评日记】项目中的UV统计实现:HyperLogLog百万数据实测,内存仅占14KB
个人主页北极的代码欢迎来访作者简介java后端学习者❄️个人专栏苍穹外卖日记SSM框架深入JavaWeb✨命运的结局尽可永在不屈的挑战却不可须臾或缺摘要本文介绍了使用Redis的HyperLogLog数据结构实现高效UV统计的方案。相比传统Set或数据库方案HyperLogLog仅需14KB内存即可统计百万级UV数据误差率仅0.24%。文章详细分析了HyperLogLog的核心原理、内存占用对比、代码实现示例并给出了项目中的架构设计和业务层代码。该方案在保证统计精度的同时内存消耗仅为Set方案的1/1000是典型的空间换时间优化案例适用于对精度要求不高的UV统计场景。一、背景什么是UV为什么要做UV统计在做网站运营时我们经常需要关注两个指标PVPage View页面访问量。用户每打开一次页面就记录一次多次打开则累加。UVUnique Visitor独立访客量。同一个用户一天内多次访问只记录一次。简单来说UV反映的是一个网站有多少「真实的人」在访问是衡量网站流量的核心指标之一。在黑马点评项目中我们需要统计每个页面的UV。但这里有个难题如何判断一个用户是否已经统计过了传统的做法是把访问过的用户ID都存起来每次访问时判断是否已存在。但如果日活是百万级存储这些用户ID就需要巨大的内存空间。二、方案选型为什么不用Set或数据库方案一数据库去重直接用数据库表存储用户ID和访问记录每次访问先查询再插入。缺点百万级UV数据库压力巨大查询耗时高。方案二Redis Set利用Redis的Set结构存储用户ID自动去重最后用SCARD获取数量。缺点如果100万个用户ID每个存8字节Long类型加上Redis内部开销大约需要15-20MB内存。20MB看起来不大但如果同时统计多个页面、多天的数据内存消耗就会成倍增长。三、终极方案Redis HyperLogLog3.1 什么是HyperLogLogHyperLogLog简称HLL是一种概率性数据结构专门用于计算集合的基数不重复元素的数量。Redis中的HLL基于String结构实现最大的特点是不管统计多少数据单个HLL的内存永远小于16KB![HLL内存占用对比图]作为代价HLL的统计结果是一个近似值误差率约为0.81%。对于UV统计来说这个误差完全可以接受。3.2 HyperLogLog核心原理简化版HLL的核心思想非常巧妙它不存储元素本身而是通过哈希概率估计算法来推算基数。哈希处理对每个元素计算64位的哈希值分桶存储64位哈希中前14位用来决定「桶编号」共16384个桶后50位用来统计「二进制连续0的个数」概率估算通过所有桶中的最大值利用数学公式估算出总的不重复元素数量这种「空间换时间」的思路让HLL在极低内存消耗下依然能保持可接受的统计精度。3.3 HyperLogLog常用命令bash# 添加元素 PFADD key element [element ...] # 统计基数 PFCOUNT key [key ...] # 合并多个HLL PFMERGE destkey sourcekey [sourcekey ...]四、代码实战百万UV统计测试为了验证HyperLogLog的实际效果我们进行一个百万级数据的压力测试。4.1 测试代码javaTest void testHyperLogLog() { // 准备100万条用户数据 String[] values new String[1000]; for (int i 0; i 1000000; i) { int j i % 1000; values[j] user_ i; if (j 999) { // 每凑够1000条批量存入Redis stringRedisTemplate.opsForHyperLogLog().add(uv:test, values); } } // 统计UV数量 Long uvCount stringRedisTemplate.opsForHyperLogLog().size(uv:test); System.out.println(统计结果 uvCount); }4.2 内存占用对比测试bash# 插入数据前查看内存 127.0.0.1:6379 INFO memory # used_memory: 905584 # 插入100万条数据后 127.0.0.1:6379 INFO memory # used_memory: 919968 # 内存增量 919968 - 905584 14384 bytes ≈ 14KB4.3 测试结果分析统计项数值实际插入数据量1,000,000HyperLogLog统计结果997,593误差率0.24%远低于理论的0.81%内存占用约14KBSet方案预估内存约15-20MB内存节省比例约1000倍结论用14KB的内存完成了百万级UV统计误差仅0.24%完全满足业务需求。五、项目中的UV统计设计总结5.1 整体架构设计text用户访问页面 → 后端接收请求 → 提取用户标识userId/IP → PFADD uv:{pageId}:{date} userId → 定时/实时统计 PFCOUNT 获取UV5.2 关键设计要点分页签统计对不同页面分别统计key格式为uv:page:{pageId}:{yyyyMMdd}分时段统计支持按天、按周、按月聚合分析合并查询使用PFMERGE实现多天UV去重统计5.3 业务层代码示例javaService public class UVStatisticsService { Autowired private StringRedisTemplate stringRedisTemplate; /** * 记录一次页面访问用于UV统计 * param pageId 页面标识 * param userId 用户ID */ public void recordUV(String pageId, Long userId) { String date LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String key uv:page: pageId : date; stringRedisTemplate.opsForHyperLogLog().add(key, userId.toString()); } /** * 获取某页面某天的UV */ public Long getUV(String pageId, LocalDate date) { String key uv:page: pageId : date.format(DateTimeFormatter.ofPattern(yyyyMMdd)); return stringRedisTemplate.opsForHyperLogLog().size(key); } /** * 获取某页面最近7天的去重UV周活跃 */ public Long getWeeklyUV(String pageId, LocalDate endDate) { String tempKey uv:temp: UUID.randomUUID(); ListString keys new ArrayList(); for (int i 0; i 7; i) { String date endDate.minusDays(i).format(DateTimeFormatter.ofPattern(yyyyMMdd)); keys.add(uv:page: pageId : date); } // 合并7天的HLL stringRedisTemplate.opsForHyperLogLog().union(tempKey, keys.toArray(new String[0])); Long result stringRedisTemplate.opsForHyperLogLog().size(tempKey); stringRedisTemplate.delete(tempKey); return result; } }六、方案优缺点分析与使用建议优点维度说明内存效率极高100万数据仅14KB性能好写入和查询都是O(1)复杂度支持合并PFMERGE可轻松实现多天去重缺点与注意事项缺点应对策略有0.81%误差对UV统计来说完全可接受无法取出具体元素需要精确用户列表时改用Set小数据量时误差偏大数据量1000时考虑降级方案选型建议做UV统计首选HyperLogLog需要精确统计且数据量小用Set需要知道具体是哪些用户用Set或数据库需要实时精确计数用BitMap如签到场景七、总结黑马点评项目中使用HyperLogLog实现UV统计是一个非常经典的空间换时间的案例传统Set方案百万UV需15-20MB内存HyperLogLog方案百万UV仅需14KB内存节省约1000倍误差仅0.24%这个案例很好地说明了没有最好的方案只有最合适的方案——根据业务场景对精度的要求选择性价比最高的技术实现。结语如果对你又帮助请点赞关注收藏你的支持就是我最大的鼓励