一、 UV 统计的业务诉求在互联网产品运营体系中用户访问量统计是衡量产品活跃度与流量规模的核心指标。其中涉及两个关键概念UVUnique Visitor独立访客量指通过互联网访问、浏览该网页的自然人数量。1 天内同一个用户多次访问该网站仅记录 1 次。UV 反映了产品的真实用户覆盖范围。PVPage View页面访问量用户每访问网站的一个页面记录 1 次 PV。用户多次打开同一页面则记录多次 PV。PV 往往用来衡量网站的总体流量与用户粘性。1.1 传统 UV 统计方案UV 统计在服务端实现较为复杂核心难点在于去重判断。系统需要判断该用户是否已经被统计过必须将已统计过的用户信息持久化保存。若采用传统关系型数据库方案需维护一张user_visit_log表记录(user_id, visit_date)的唯一索引。当日活用户突破百万级时该表将产生以下问题存储空间爆炸百万级用户 × 365 天 3.65 亿条记录索引与数据文件占用数十 GB 磁盘空间。写入性能劣化每次用户访问需执行INSERT IGNORE或ON DUPLICATE KEY UPDATEB 树索引频繁分裂与合并数据库 CPU 与 IO 负载居高不下。查询延迟飙升统计某日 UV 需执行SELECT COUNT(DISTINCT user_id) FROM user_visit_log WHERE date ?全表扫描与临时表排序导致响应时间呈指数级上升。若采用 Redis Set 结构存储每日访问用户 ID虽可将查询延迟压缩至毫秒级但内存消耗依然恐怖。假设日活用户 1000 万每个用户 ID 占用 8 字节Long 类型单日数据即需 80MB 内存。全年累计需 29GB 内存成本高昂且不可持续。二、 HyperLogLog 的优势2.1 算法原理与核心特性HyperLogLogHLL是从 LogLog 算法派生的概率算法用于确定非常大的集合的基数Cardinality而不需要存储其所有值。其核心思想是利用概率统计与哈希函数的均匀分布特性通过观察哈希值的二进制模式中前导零的数量估算集合中不同元素的数量。Redis 中的 HLL 是基于 String 结构实现的单个 HLL 的内存占用永远小于 16KB。这一极致的内存压缩比使得 HyperLogLog 成为海量数据去重统计的不二之选。代价与权衡作为概率算法HyperLogLog 的测量结果存在小于0.81%的标准误差。但对于 UV 统计这类业务场景而言这一误差完全可以忽略。运营人员关注的是流量趋势与量级而非精确到个位数的统计结果。2.2 Redis HyperLogLog 核心命令命令功能描述时间复杂度PFADD key element [element ...]添加一个或多个元素到 HyperLogLogO(1)PFCOUNT key [key ...]计算一个或多个 HyperLogLog 的并集基数O(N)PFMERGE destkey sourcekey [sourcekey ...]将多个 HyperLogLog 合并为一个O(N)命令详解PFADD向指定 Key 的 HyperLogLog 中添加元素。若元素已存在不会重复计数。返回值为 1 表示 HyperLogLog 被修改新增元素0 表示元素已存在。PFCOUNT返回指定 Key 的估算基数。支持传入多个 Key返回它们的并集去重数量适用于跨天、跨维度的合并统计。PFMERGE将多个源 HyperLogLog 合并到目标 Key 中适用于数据归档与离线分析。三、 UV 统计实现与压测验证3.1 单元测试压测代码我们通过单元测试向 HyperLogLog 中添加 100 万条数据验证其内存占用与统计精度importredisimporttimedeftest_hyperloglog():# 连接 Redisrredis.Redis(host127.0.0.1,port6379,db0,decode_responsesTrue)# 清空测试 Keyr.delete(hll:uv:daily)# 准备批量添加batch_size1000total_users1000000start_timetime.time()# 批量添加 100 万用户foriinrange(0,total_users,batch_size):users[fuser_{j}forjinrange(i1,min(ibatch_size1,total_users1))]r.pfadd(hll:uv:daily,*users)# 统计数量uv_countr.pfcount(hll:uv:daily)# 获取内存占用memory_usedr.memory_usage(hll:uv:daily)elapsed_timetime.time()-start_timeprint(f实际添加用户数:{total_users})print(fHyperLogLog 统计结果:{uv_count})print(f误差率:{abs(uv_count-total_users)/total_users*100:.4f}%)print(f内存占用:{memory_used}bytes ({memory_used/1024:.2f}KB))print(f耗时:{elapsed_time:.4f}秒)print(f吞吐量:{total_users/elapsed_time:.0f}ops/sec)if__name____main__:test_hyperloglog()压测结果分析典型输出实际添加用户数: 1000000 HyperLogLog 统计结果: 1008542 误差率: 0.8542% 内存占用: 12288 bytes (12.00 KB) 耗时: 2.3456 秒 吞吐量: 426315 ops/sec结论内存占用仅 12KB远低于 Set 结构的 80MB100 万用户。统计精度误差率 0.85%符合 HyperLogLog 的标准误差范围 0.81% 为理论值实际略有波动。写入性能每秒可处理 42 万次添加操作完全满足高并发场景。3.2 UV 统计架构设计Key 设计规范# 日级 UV 统计uv:daily:{YYYY-MM-DD}# 例如: uv:daily:2024-01-15# 月级 UV 统计通过 PFCOUNT 合并日级数据uv:monthly:{YYYY-MM}# 例如: uv:monthly:2024-01# 全站历史 UV通过 PFMERGE 合并月级数据uv:lifetime核心实现代码importredisfromdatetimeimportdatetime,timedeltafromtypingimportListclassUVStatisticsService:def__init__(self,redis_client:redis.Redis):self.redisredis_clientdefrecord_visit(self,user_id:int,visit_date:datetimeNone): 记录用户访问 :param user_id: 用户 ID :param visit_date: 访问日期默认当天 ifvisit_dateisNone:visit_datedatetime.now()date_keyvisit_date.strftime(%Y-%m-%d)keyfuv:daily:{date_key}# 添加用户到当日 HyperLogLogself.redis.pfadd(key,str(user_id))# 设置过期时间保留 90 天数据self.redis.expire(key,90*24*60*60)defget_daily_uv(self,date:datetimeNone)-int: 获取指定日期的 UV :param date: 查询日期默认当天 :return: UV 数量 ifdateisNone:datedatetime.now()date_keydate.strftime(%Y-%m-%d)keyfuv:daily:{date_key}returnself.redis.pfcount(key)defget_date_range_uv(self,start_date:datetime,end_date:datetime)-int: 获取日期范围内的 UV并集统计 :param start_date: 开始日期 :param end_date: 结束日期 :return: 去重后的 UV 数量 keys[]current_datestart_datewhilecurrent_dateend_date:keyfuv:daily:{current_date.strftime(%Y-%m-%d)}keys.append(key)current_datetimedelta(days1)ifnotkeys:return0# 使用 PFCOUNT 计算并集基数returnself.redis.pfcount(*keys)defmerge_monthly_uv(self,year:int,month:int): 合并月度 UV 统计 :param year: 年份 :param month: 月份 # 生成该月所有日期的 Keykeys[]datedatetime(year,month,1)whiledate.monthmonth:keyfuv:daily:{date.strftime(%Y-%m-%d)}keys.append(key)datetimedelta(days1)# 合并到月度 Keymonthly_keyfuv:monthly:{year}-{month:02d}self.redis.pfmerge(monthly_key,*keys)# 设置过期时间保留 2 年self.redis.expire(monthly_key,2*365*24*60*60)FastAPI 接口实现fromfastapiimportAPIRouter,Depends,HTTPExceptionfromdatetimeimportdatetime,timedeltafrompydanticimportBaseModel routerAPIRouter()classUVStatsResponse(BaseModel):daily_uv:intweekly_uv:intmonthly_uv:introuter.get(/stats/uv,response_modelUVStatsResponse)asyncdefget_uv_statistics(date:strNone,uv_service:UVStatisticsServiceDepends(lambda:UVStatisticsService(redis_client))): 获取 UV 统计数据 :param date: 查询日期YYYY-MM-DD 格式默认当天 :return: 日、周、月 UV 统计 ifdate:try:query_datedatetime.strptime(date,%Y-%m-%d)exceptValueError:raiseHTTPException(status_code400,detailInvalid date format)else:query_datedatetime.now()# 日 UVdaily_uvuv_service.get_daily_uv(query_date)# 周 UV最近 7 天week_startquery_date-timedelta(days6)weekly_uvuv_service.get_date_range_uv(week_start,query_date)# 月 UV最近 30 天month_startquery_date-timedelta(days29)monthly_uvuv_service.get_date_range_uv(month_start,query_date)returnUVStatsResponse(daily_uvdaily_uv,weekly_uvweekly_uv,monthly_uvmonthly_uv)router.post(/visit/record)asyncdefrecord_user_visit(user_id:int,uv_service:UVStatisticsServiceDepends(lambda:UVStatisticsService(redis_client))): 记录用户访问 :param user_id: 用户 ID uv_service.record_visit(user_id)return{status:success,message:Visit recorded}四、 HyperLogLog 与 Bitmap、Set 的对比选型数据结构内存占用精确度适用场景核心命令HyperLogLog 16KB固定误差 0.81%海量 UV 统计、去重计数PFADD,PFCOUNTBitmapN bitsN最大值100% 精确连续整数 ID 签到、状态标记SETBIT,GETBIT,BITCOUNTSetN × 8 bytes100% 精确中小规模去重集合、交集/并集运算SADD,SISMEMBER,SINTER选型建议UV 统计千万级以上HyperLogLog内存占用极低误差可接受。用户签到连续日期Bitmap按日期偏移量存储支持连续签到统计。共同关注/好友列表万级以下Set支持交集、并集等集合运算精确度高。五、 总结5.1 核心收益极致内存效率单日 UV 统计仅需 12KB 内存全年 365 天累计仅 4.5MB相比 Set 结构节省 99.99% 内存。高性能写入单机 Redis 可支撑百万级 QPS 的 PV 记录满足亿级日活产品的统计需求。灵活聚合能力通过PFCOUNT与PFMERGE实现跨天、跨维度的并集统计支持周报、月报等复杂分析场景。5.2 优化建议TTL 过期策略为日级 UV Key 设置 90 天 TTL自动清理历史数据防止内存无限增长。月度归档通过定时任务执行PFMERGE将日级数据合并为月度 Key便于长期趋势分析。误差容忍业务层需明确 HyperLogLog 的误差特性避免在财务结算等强一致性场景使用。监控告警监控 Redis 内存使用率与 HyperLogLog Key 数量设置阈值告警防止内存溢出。
Redis HyperLogLog用户统计功能实现
一、 UV 统计的业务诉求在互联网产品运营体系中用户访问量统计是衡量产品活跃度与流量规模的核心指标。其中涉及两个关键概念UVUnique Visitor独立访客量指通过互联网访问、浏览该网页的自然人数量。1 天内同一个用户多次访问该网站仅记录 1 次。UV 反映了产品的真实用户覆盖范围。PVPage View页面访问量用户每访问网站的一个页面记录 1 次 PV。用户多次打开同一页面则记录多次 PV。PV 往往用来衡量网站的总体流量与用户粘性。1.1 传统 UV 统计方案UV 统计在服务端实现较为复杂核心难点在于去重判断。系统需要判断该用户是否已经被统计过必须将已统计过的用户信息持久化保存。若采用传统关系型数据库方案需维护一张user_visit_log表记录(user_id, visit_date)的唯一索引。当日活用户突破百万级时该表将产生以下问题存储空间爆炸百万级用户 × 365 天 3.65 亿条记录索引与数据文件占用数十 GB 磁盘空间。写入性能劣化每次用户访问需执行INSERT IGNORE或ON DUPLICATE KEY UPDATEB 树索引频繁分裂与合并数据库 CPU 与 IO 负载居高不下。查询延迟飙升统计某日 UV 需执行SELECT COUNT(DISTINCT user_id) FROM user_visit_log WHERE date ?全表扫描与临时表排序导致响应时间呈指数级上升。若采用 Redis Set 结构存储每日访问用户 ID虽可将查询延迟压缩至毫秒级但内存消耗依然恐怖。假设日活用户 1000 万每个用户 ID 占用 8 字节Long 类型单日数据即需 80MB 内存。全年累计需 29GB 内存成本高昂且不可持续。二、 HyperLogLog 的优势2.1 算法原理与核心特性HyperLogLogHLL是从 LogLog 算法派生的概率算法用于确定非常大的集合的基数Cardinality而不需要存储其所有值。其核心思想是利用概率统计与哈希函数的均匀分布特性通过观察哈希值的二进制模式中前导零的数量估算集合中不同元素的数量。Redis 中的 HLL 是基于 String 结构实现的单个 HLL 的内存占用永远小于 16KB。这一极致的内存压缩比使得 HyperLogLog 成为海量数据去重统计的不二之选。代价与权衡作为概率算法HyperLogLog 的测量结果存在小于0.81%的标准误差。但对于 UV 统计这类业务场景而言这一误差完全可以忽略。运营人员关注的是流量趋势与量级而非精确到个位数的统计结果。2.2 Redis HyperLogLog 核心命令命令功能描述时间复杂度PFADD key element [element ...]添加一个或多个元素到 HyperLogLogO(1)PFCOUNT key [key ...]计算一个或多个 HyperLogLog 的并集基数O(N)PFMERGE destkey sourcekey [sourcekey ...]将多个 HyperLogLog 合并为一个O(N)命令详解PFADD向指定 Key 的 HyperLogLog 中添加元素。若元素已存在不会重复计数。返回值为 1 表示 HyperLogLog 被修改新增元素0 表示元素已存在。PFCOUNT返回指定 Key 的估算基数。支持传入多个 Key返回它们的并集去重数量适用于跨天、跨维度的合并统计。PFMERGE将多个源 HyperLogLog 合并到目标 Key 中适用于数据归档与离线分析。三、 UV 统计实现与压测验证3.1 单元测试压测代码我们通过单元测试向 HyperLogLog 中添加 100 万条数据验证其内存占用与统计精度importredisimporttimedeftest_hyperloglog():# 连接 Redisrredis.Redis(host127.0.0.1,port6379,db0,decode_responsesTrue)# 清空测试 Keyr.delete(hll:uv:daily)# 准备批量添加batch_size1000total_users1000000start_timetime.time()# 批量添加 100 万用户foriinrange(0,total_users,batch_size):users[fuser_{j}forjinrange(i1,min(ibatch_size1,total_users1))]r.pfadd(hll:uv:daily,*users)# 统计数量uv_countr.pfcount(hll:uv:daily)# 获取内存占用memory_usedr.memory_usage(hll:uv:daily)elapsed_timetime.time()-start_timeprint(f实际添加用户数:{total_users})print(fHyperLogLog 统计结果:{uv_count})print(f误差率:{abs(uv_count-total_users)/total_users*100:.4f}%)print(f内存占用:{memory_used}bytes ({memory_used/1024:.2f}KB))print(f耗时:{elapsed_time:.4f}秒)print(f吞吐量:{total_users/elapsed_time:.0f}ops/sec)if__name____main__:test_hyperloglog()压测结果分析典型输出实际添加用户数: 1000000 HyperLogLog 统计结果: 1008542 误差率: 0.8542% 内存占用: 12288 bytes (12.00 KB) 耗时: 2.3456 秒 吞吐量: 426315 ops/sec结论内存占用仅 12KB远低于 Set 结构的 80MB100 万用户。统计精度误差率 0.85%符合 HyperLogLog 的标准误差范围 0.81% 为理论值实际略有波动。写入性能每秒可处理 42 万次添加操作完全满足高并发场景。3.2 UV 统计架构设计Key 设计规范# 日级 UV 统计uv:daily:{YYYY-MM-DD}# 例如: uv:daily:2024-01-15# 月级 UV 统计通过 PFCOUNT 合并日级数据uv:monthly:{YYYY-MM}# 例如: uv:monthly:2024-01# 全站历史 UV通过 PFMERGE 合并月级数据uv:lifetime核心实现代码importredisfromdatetimeimportdatetime,timedeltafromtypingimportListclassUVStatisticsService:def__init__(self,redis_client:redis.Redis):self.redisredis_clientdefrecord_visit(self,user_id:int,visit_date:datetimeNone): 记录用户访问 :param user_id: 用户 ID :param visit_date: 访问日期默认当天 ifvisit_dateisNone:visit_datedatetime.now()date_keyvisit_date.strftime(%Y-%m-%d)keyfuv:daily:{date_key}# 添加用户到当日 HyperLogLogself.redis.pfadd(key,str(user_id))# 设置过期时间保留 90 天数据self.redis.expire(key,90*24*60*60)defget_daily_uv(self,date:datetimeNone)-int: 获取指定日期的 UV :param date: 查询日期默认当天 :return: UV 数量 ifdateisNone:datedatetime.now()date_keydate.strftime(%Y-%m-%d)keyfuv:daily:{date_key}returnself.redis.pfcount(key)defget_date_range_uv(self,start_date:datetime,end_date:datetime)-int: 获取日期范围内的 UV并集统计 :param start_date: 开始日期 :param end_date: 结束日期 :return: 去重后的 UV 数量 keys[]current_datestart_datewhilecurrent_dateend_date:keyfuv:daily:{current_date.strftime(%Y-%m-%d)}keys.append(key)current_datetimedelta(days1)ifnotkeys:return0# 使用 PFCOUNT 计算并集基数returnself.redis.pfcount(*keys)defmerge_monthly_uv(self,year:int,month:int): 合并月度 UV 统计 :param year: 年份 :param month: 月份 # 生成该月所有日期的 Keykeys[]datedatetime(year,month,1)whiledate.monthmonth:keyfuv:daily:{date.strftime(%Y-%m-%d)}keys.append(key)datetimedelta(days1)# 合并到月度 Keymonthly_keyfuv:monthly:{year}-{month:02d}self.redis.pfmerge(monthly_key,*keys)# 设置过期时间保留 2 年self.redis.expire(monthly_key,2*365*24*60*60)FastAPI 接口实现fromfastapiimportAPIRouter,Depends,HTTPExceptionfromdatetimeimportdatetime,timedeltafrompydanticimportBaseModel routerAPIRouter()classUVStatsResponse(BaseModel):daily_uv:intweekly_uv:intmonthly_uv:introuter.get(/stats/uv,response_modelUVStatsResponse)asyncdefget_uv_statistics(date:strNone,uv_service:UVStatisticsServiceDepends(lambda:UVStatisticsService(redis_client))): 获取 UV 统计数据 :param date: 查询日期YYYY-MM-DD 格式默认当天 :return: 日、周、月 UV 统计 ifdate:try:query_datedatetime.strptime(date,%Y-%m-%d)exceptValueError:raiseHTTPException(status_code400,detailInvalid date format)else:query_datedatetime.now()# 日 UVdaily_uvuv_service.get_daily_uv(query_date)# 周 UV最近 7 天week_startquery_date-timedelta(days6)weekly_uvuv_service.get_date_range_uv(week_start,query_date)# 月 UV最近 30 天month_startquery_date-timedelta(days29)monthly_uvuv_service.get_date_range_uv(month_start,query_date)returnUVStatsResponse(daily_uvdaily_uv,weekly_uvweekly_uv,monthly_uvmonthly_uv)router.post(/visit/record)asyncdefrecord_user_visit(user_id:int,uv_service:UVStatisticsServiceDepends(lambda:UVStatisticsService(redis_client))): 记录用户访问 :param user_id: 用户 ID uv_service.record_visit(user_id)return{status:success,message:Visit recorded}四、 HyperLogLog 与 Bitmap、Set 的对比选型数据结构内存占用精确度适用场景核心命令HyperLogLog 16KB固定误差 0.81%海量 UV 统计、去重计数PFADD,PFCOUNTBitmapN bitsN最大值100% 精确连续整数 ID 签到、状态标记SETBIT,GETBIT,BITCOUNTSetN × 8 bytes100% 精确中小规模去重集合、交集/并集运算SADD,SISMEMBER,SINTER选型建议UV 统计千万级以上HyperLogLog内存占用极低误差可接受。用户签到连续日期Bitmap按日期偏移量存储支持连续签到统计。共同关注/好友列表万级以下Set支持交集、并集等集合运算精确度高。五、 总结5.1 核心收益极致内存效率单日 UV 统计仅需 12KB 内存全年 365 天累计仅 4.5MB相比 Set 结构节省 99.99% 内存。高性能写入单机 Redis 可支撑百万级 QPS 的 PV 记录满足亿级日活产品的统计需求。灵活聚合能力通过PFCOUNT与PFMERGE实现跨天、跨维度的并集统计支持周报、月报等复杂分析场景。5.2 优化建议TTL 过期策略为日级 UV Key 设置 90 天 TTL自动清理历史数据防止内存无限增长。月度归档通过定时任务执行PFMERGE将日级数据合并为月度 Key便于长期趋势分析。误差容忍业务层需明确 HyperLogLog 的误差特性避免在财务结算等强一致性场景使用。监控告警监控 Redis 内存使用率与 HyperLogLog Key 数量设置阈值告警防止内存溢出。