Redis 如何统计独立用户访问量?

Redis 如何统计独立用户访问量?(UV 统计的 4 种方案)

在网站分析、广告监测、推荐系统等场景中,独立用户访问量(UV,Unique Visitor) 是一个核心指标。UV 的关键在于去重------同一个用户多次访问只计一次。

Redis 提供了多种数据结构来高效实现 UV 统计,各有优劣。本文将详细对比 Set、Bitmap、HyperLogLog、incr + 日期维度(即用户提到的两种方式)四种方案,并通过流程图和代码示例帮助你选型。

一、方案概览(附选型流程图)

整数且连续

如user_id=1~1亿
字符串或稀疏ID

如uuid/手机号
允许0.81%误差

如大屏展示
必须精确
需要每日独立统计
需要统计UV
用户ID类型?
Bitmap方案

内存极小, 准确
是否允许微小误差?
HyperLogLog方案

内存12KB, 性能极高
Set方案

内存随基数增长
能否预聚合?
incr + 日期维度

辅助计数, 需配合Set/HLL

二、方案一:Set 集合(精确去重)

最直观的方法:每个统计周期(如一天)维护一个 Set,将每个访问过的用户 ID 加入 Set,最后用 SCARD 获取基数。

python 复制代码
# 示例:用户 1001 访问首页
redis.sadd("uv:home:2025-04-15", "user_1001")
# 获取当天 UV
uv = redis.scard("uv:home:2025-04-15")

优点 :精确、支持用户 ID 任意类型(字符串/整数)。
缺点 :内存占用高,每个用户 ID 都需要存储一份(例如 1000 万用户,每个 ID 按 30 字节算,需约 300MB)。
适用:用户量小(< 百万级)或必须精确统计的场景。

三、方案二:Bitmap(位图法,精确且内存极省)

当用户 ID 是整数且相对连续(如自增 user_id)时,可以用 Bitmap 将每个 user_id 映射到位偏移量,存在则置 1。

python 复制代码
# 用户 ID=1001 访问,设置第 1001 位为 1
redis.setbit("uv:home:2025-04-15", 1001, 1)
# 统计当日 UV(统计 1 的个数)
uv = redis.bitcount("uv:home:2025-04-15")

内存计算 :如果有 1 亿用户,只需 1亿 bit ≈ 12 MB,比 Set 节省数十倍。
优点 :精确、内存极小、性能高(bitcount 时间复杂度 O(n) 但 Redis 做了优化)。
缺点 :用户 ID 必须为整数且不太稀疏(若 ID 最大为 10 亿,但实际只有 100 万用户,依然会占用 125MB 的连续空间,造成浪费)。
适用:用户 ID 是自增整数、最大 ID 可控(如 2^32 以内)、对内存敏感且要求精确的场景。

四、方案三:HyperLogLog(近似去重,误差 0.81%)

你提到的 HyperLogLog 是一种概率性数据结构,用 12KB 固定内存即可统计上亿级别的 UV,误差率约为 0.81%。

python 复制代码
# 添加元素
redis.pfadd("uv:home:2025-04-15", "user_1001", "user_1002")
# 获取近似 UV
uv = redis.pfcount("uv:home:2025-04-15")

原理 :通过哈希函数将元素映射为二进制串,观察低位连续零的个数来估计基数。
优点 :内存固定(12KB),性能极高(O(1) 添加),适合海量数据。
缺点 :不精确(误差 ±0.81%),无法取出具体有哪些用户(只能计数),不适合敏感计费场景。
适用:大屏展示、趋势分析、非精准营销统计等可容忍误差的场景。

五、方案四:incr + 日期维度(你提到的"incr自增")

严格来说,单纯使用 INCR 无法实现独立用户去重,因为 INCR 是累加计数器,每次访问都 +1,得到的是 PV(页面访问量),不是 UV。

python 复制代码
# 这样得到的是 PV,不是 UV
redis.incr("pv:home:2025-04-15")

如何用 incr 辅助 UV?

通常做法是 incr + Set/Bitmap/HLL 组合

  • 用 Set 或 HLL 存储独立用户(保证去重)
  • 同时用 incr 记录总访问次数(PV)
python 复制代码
# 记录 PV
redis.incr("pv:home:2025-04-15")
# 记录 UV(使用 HLL)
redis.pfadd("uv:home:2025-04-15", user_id)

所以,你提到的"incr 通过自增方式判断用户的访问量"并不适用于 UV,应理解为 PV 统计。但为了贴合你的原文,我们修正说明:incr 适合 PV,UV 必须依赖去重结构

六、四种方案对比表

方案 内存占用 精确性 支持用户ID类型 时间复杂度(写入) 典型应用
Set O(N)(每个元素完整存储) 精确 任意 O(1) 小规模精确统计
Bitmap O(max_id) 位,连续整数时极省 精确 非负整数 O(1) 亿级整数ID,如手机号后几位
HyperLogLog 固定 12KB 近似(误差 0.81%) 任意(需哈希) O(1) 海量UV快速估算
incr(PV) 固定(每个key一个整数) 精确 无(只是计数) O(1) 页面访问总量(非UV)

七、实战选型建议

  • 你的用户 ID 是整数且密集(如 user_id 从 1 到 5000 万)

    👉 首选 Bitmap,精确且内存最小。

  • 用户 ID 是字符串(如 UUID、手机号),且允许 0.81% 误差

    👉 首选 HyperLogLog,12KB 内存统计上亿 UV。

  • 必须精确统计,且用户量较小(< 500 万)

    👉 用 Set,简单可靠。

  • 既要 PV 又要 UV

    👉 组合:INCR 记录 PV + PFADD 记录 UV(HLL)或 SADD(Set)。

  • 数据敏感场景(如计费、反作弊)

    ❌ 不能用 HyperLogLog,必须用 Bitmap 或 Set。

八、代码示例:三种方案对比(Python + Redis)

python 复制代码
import redis
r = redis.Redis(decode_responses=True)

# 模拟 100 万个用户 ID(字符串)
user_ids = [f"user_{i}" for i in range(1_000_000)]

# 1. Set 方式
key_set = "uv:set"
r.delete(key_set)
for uid in user_ids:
    r.sadd(key_set, uid)
print(f"Set 精确 UV: {r.scard(key_set)}")
print(f"Set 内存: {r.memory_usage(key_set) / 1024 / 1024:.2f} MB")

# 2. HyperLogLog 方式
key_hll = "uv:hll"
r.delete(key_hll)
for uid in user_ids:
    r.pfadd(key_hll, uid)
print(f"HLL 近似 UV: {r.pfcount(key_hll)}")
print(f"HLL 内存: {r.memory_usage(key_hll)} 字节")  # 固定约 12KB

# 3. Bitmap 方式(假设 user_id 转为整数,此处用 i 模拟)
key_bit = "uv:bitmap"
r.delete(key_bit)
for i in range(1, 1_000_001):
    r.setbit(key_bit, i, 1)
print(f"Bitmap 精确 UV: {r.bitcount(key_bit)}")
print(f"Bitmap 内存: {r.memory_usage(key_bit) / 1024 / 1024:.2f} MB")

运行结果参考(百万级):

  • Set:内存约 30~40 MB
  • HLL:12 KB
  • Bitmap:0.12 MB(100 万 bit = 0.125 MB)

九、总结

你的原始说法 修正/补充
"incr 通过自增方式判断用户的访问量" incr 得到的是 PV(总访问次数),不是 UV。UV 需要去重。
"HyperLogLog 用来做基数统计,误差很小,不适合数据敏感场景" ✅ 正确。误差约 0.81%,内存固定 12KB,适合海量近似统计。

最终结论

  • 对精度要求不高、数据量极大 → HyperLogLog
  • 需要精确、用户 ID 为整数 → Bitmap
  • 需要精确、用户 ID 为字符串且量小 → Set
  • 想要统计 PV → incr

合理选择数据结构,能让你的 UV 统计既快又省内存。


📌 扩展思考:如果既要精确又要省内存,可以结合分段 Bitmap + 冷热分离,但会增加复杂度。有没有更好的思路?

相关推荐
脑子加油站2 小时前
OpenEuler24.03 分布式配置redis 集群
数据库·redis·分布式·php·nginx代理
ZC跨境爬虫4 小时前
纯requests+Redis实现分布式爬虫(可视化4终端,模拟4台电脑联合爬取)
redis·分布式·爬虫·python
Cat_Rocky14 小时前
redis哨兵模式
数据库·redis
披着羊皮不是狼18 小时前
(7)为 RAG 系统接入 Redis Stack 实现向量持久化
数据库·redis·缓存
XDHCOM19 小时前
Docker怎么设置Redis?
redis·docker·容器
ruan11451421 小时前
Redis--个人学习记录
数据库·redis·学习
Micro麦可乐21 小时前
Redis只会用来做缓存?解锁Redis非缓存的九个应用场景,90%程序员不知道的隐藏技能
数据库·redis·缓存·消息队列·分布式锁·延迟队列·布隆过滤器
21号 121 小时前
10.Redis 缓存
数据库·redis·缓存
从零开始的-CodeNinja之路21 小时前
【Redis】Redis 缓存应用、淘汰机制—(四)
java·redis·缓存