当前位置: 首页 > news >正文

Redis HyperLogLog用户统计功能实现

一、 UV 统计的业务诉求

在互联网产品运营体系中,用户访问量统计是衡量产品活跃度与流量规模的核心指标。其中涉及两个关键概念:

UV(Unique Visitor)独立访客量
指通过互联网访问、浏览该网页的自然人数量。1 天内同一个用户多次访问该网站,仅记录 1 次。UV 反映了产品的真实用户覆盖范围。

PV(Page View)页面访问量
用户每访问网站的一个页面,记录 1 次 PV。用户多次打开同一页面,则记录多次 PV。PV 往往用来衡量网站的总体流量与用户粘性。

1.1 传统 UV 统计方案

UV 统计在服务端实现较为复杂,核心难点在于去重判断。系统需要判断该用户是否已经被统计过,必须将已统计过的用户信息持久化保存。

若采用传统关系型数据库方案,需维护一张user_visit_log表,记录(user_id, visit_date)的唯一索引。当日活用户突破百万级时,该表将产生以下问题:

  • 存储空间爆炸:百万级用户 × 365 天 = 3.65 亿条记录,索引与数据文件占用数十 GB 磁盘空间。
  • 写入性能劣化:每次用户访问需执行INSERT IGNOREON DUPLICATE KEY UPDATE,B+ 树索引频繁分裂与合并,数据库 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 算法原理与核心特性

HyperLogLog(HLL)是从 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():# 连接 Redisr=redis.Redis(host='127.0.0.1',port=6379,db=0,decode_responses=True)# 清空测试 Keyr.delete('hll:uv:daily')# 准备批量添加batch_size=1000total_users=1000000start_time=time.time()# 批量添加 100 万用户foriinrange(0,total_users,batch_size):users=[f"user_{j}"forjinrange(i+1,min(i+batch_size+1,total_users+1))]r.pfadd('hll:uv:daily',*users)# 统计数量uv_count=r.pfcount('hll:uv:daily')# 获取内存占用memory_used=r.memory_usage('hll:uv:daily')elapsed_time=time.time()-start_timeprint(f"实际添加用户数:{total_users}")print(f"HyperLogLog 统计结果:{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 结构的 80MB(100 万用户)。
  • 统计精度:误差率 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.redis=redis_clientdefrecord_visit(self,user_id:int,visit_date:datetime=None):""" 记录用户访问 :param user_id: 用户 ID :param visit_date: 访问日期(默认当天) """ifvisit_dateisNone:visit_date=datetime.now()date_key=visit_date.strftime("%Y-%m-%d")key=f"uv:daily:{date_key}"# 添加用户到当日 HyperLogLogself.redis.pfadd(key,str(user_id))# 设置过期时间(保留 90 天数据)self.redis.expire(key,90*24*60*60)defget_daily_uv(self,date:datetime=None)->int:""" 获取指定日期的 UV :param date: 查询日期(默认当天) :return: UV 数量 """ifdateisNone:date=datetime.now()date_key=date.strftime("%Y-%m-%d")key=f"uv: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_date=start_datewhilecurrent_date<=end_date:key=f"uv:daily:{current_date.strftime('%Y-%m-%d')}"keys.append(key)current_date+=timedelta(days=1)ifnotkeys:return0# 使用 PFCOUNT 计算并集基数returnself.redis.pfcount(*keys)defmerge_monthly_uv(self,year:int,month:int):""" 合并月度 UV 统计 :param year: 年份 :param month: 月份 """# 生成该月所有日期的 Keykeys=[]date=datetime(year,month,1)whiledate.month==month:key=f"uv:daily:{date.strftime('%Y-%m-%d')}"keys.append(key)date+=timedelta(days=1)# 合并到月度 Keymonthly_key=f"uv: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 router=APIRouter()classUVStatsResponse(BaseModel):daily_uv:intweekly_uv:intmonthly_uv:int@router.get("/stats/uv",response_model=UVStatsResponse)asyncdefget_uv_statistics(date:str=None,uv_service:UVStatisticsService=Depends(lambda:UVStatisticsService(redis_client))):""" 获取 UV 统计数据 :param date: 查询日期(YYYY-MM-DD 格式,默认当天) :return: 日、周、月 UV 统计 """ifdate:try:query_date=datetime.strptime(date,"%Y-%m-%d")exceptValueError:raiseHTTPException(status_code=400,detail="Invalid date format")else:query_date=datetime.now()# 日 UVdaily_uv=uv_service.get_daily_uv(query_date)# 周 UV(最近 7 天)week_start=query_date-timedelta(days=6)weekly_uv=uv_service.get_date_range_uv(week_start,query_date)# 月 UV(最近 30 天)month_start=query_date-timedelta(days=29)monthly_uv=uv_service.get_date_range_uv(month_start,query_date)returnUVStatsResponse(daily_uv=daily_uv,weekly_uv=weekly_uv,monthly_uv=monthly_uv)@router.post("/visit/record")asyncdefrecord_user_visit(user_id:int,uv_service:UVStatisticsService=Depends(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,PFCOUNT
BitmapN bits(N=最大值)100% 精确连续整数 ID 签到、状态标记SETBIT,GETBIT,BITCOUNT
SetN × 8 bytes100% 精确中小规模去重集合、交集/并集运算SADD,SISMEMBER,SINTER

选型建议

  • UV 统计(千万级以上):HyperLogLog,内存占用极低,误差可接受。
  • 用户签到(连续日期):Bitmap,按日期偏移量存储,支持连续签到统计。
  • 共同关注/好友列表(万级以下):Set,支持交集、并集等集合运算,精确度高。

五、 总结

5.1 核心收益

  1. 极致内存效率:单日 UV 统计仅需 12KB 内存,全年 365 天累计仅 4.5MB,相比 Set 结构节省 99.99% 内存。
  2. 高性能写入:单机 Redis 可支撑百万级 QPS 的 PV 记录,满足亿级日活产品的统计需求。
  3. 灵活聚合能力:通过PFCOUNTPFMERGE实现跨天、跨维度的并集统计,支持周报、月报等复杂分析场景。

5.2 优化建议

  1. TTL 过期策略:为日级 UV Key 设置 90 天 TTL,自动清理历史数据,防止内存无限增长。
  2. 月度归档:通过定时任务执行PFMERGE,将日级数据合并为月度 Key,便于长期趋势分析。
  3. 误差容忍:业务层需明确 HyperLogLog 的误差特性,避免在财务结算等强一致性场景使用。
  4. 监控告警:监控 Redis 内存使用率与 HyperLogLog Key 数量,设置阈值告警,防止内存溢出。
http://www.cnnetsun.cn/news/2747107.html

相关文章:

  • 基于Arduino Nano的智能小车PCB设计:从传感器集成到自主避障
  • Halcon实战:用decompose3和trans_from_rgb搞定彩色图像分割与HSV转换(附避坑要点)
  • 相位测距信号处理实战:如何用混频和FFT把15MHz高频信号‘降频’测准相位?
  • MATLAB实现高斯混合背景建模的运动目标检测与框选跟踪代码包
  • WebPlotDigitizer完整指南:科研图表数据提取的终极解决方案
  • 基于树莓派Zero W的微型侦察机器人:从零构建嵌入式移动平台
  • 跨平台网盘文件直链解析工具:告别客户端依赖的现代化下载方案
  • 从向量与嵌入到ChromaDB:构建AI应用的语义搜索基石
  • GPT-5.5 Pro与DeepSeek-V4实战对比:逻辑推理、工程交付与协作范式
  • 别再只盯着数据了!手把手教你用新拓三维XTDIC系统做一次靠谱的精度验证实验
  • Windows 11 LTSC版安装微软商店的完整指南:3分钟快速恢复应用生态
  • GoSkills:Go语言原生Claude技能包运行时详解
  • 从Verilog到可执行程序:手把手教你用Verilator在Ubuntu 22.04上构建你的第一个硬件模拟器
  • 别再只盯着K因子了!ADS实战:用环路增益和奈奎斯特图给你的射频放大器“体检”
  • 手把手教你用STM32F407的SDIO给TF卡建个‘文件系统’,告别裸读写
  • 告别环境配置焦虑:用VS2022和OpenCV 4.9.0,5分钟搞定你的第一个图像识别Demo
  • 基于Arduino与433MHz射频模块的单向无线通信系统搭建指南
  • 从静态滑翔机到遥控飞机:DIY改装全流程与核心技术解析
  • Django搭建的轻量级图书借阅后台,含用户管理、借还登记与库存统计功能
  • Ripes:可视化RISC-V处理器模拟器,让硬件学习变得触手可及
  • RV1126人脸识别项目实战:手把手教你搞定GC2053红外摄像头驱动配置与VLC拉流
  • 为什么87%的RAG项目在对话整合阶段失败?一线专家复盘6类典型架构断裂场景
  • STM32H743VIT6最小系统板AD工程包:原理图+PCB+封装库全开源
  • AI工具如何真正接管内容风控?揭秘头部平台智能审核系统日均拦截99.98%违规内容的技术闭环
  • 黑龙江全省三级行政区划矢量数据:地级市、区县、乡镇街道边界SHP文件合集
  • 为你的RB5机器人系统加把锁:详解dm-verity验证与FBE加密配置
  • SAP-ABAP:S/4HANA 下的 ST02 深度解读:从缓冲区监控到内存架构优化
  • 【完整题单10、贪心与思维(区间合并)】【✅✅✅✅】
  • 如何高效解密NCM文件?ncmdumpGUI完整指南助你解放音乐收藏
  • [MAF预定义的AIContextProvider-07]FileAccessProvider——为Agent提供文件读写能力