在互联网应用中,统计大量用户的唯一访问量(UV) 是一个常见且重要的需求。Redis 提供了多种高效的数据结构和算法来实现这一目标,以下是常见的几种方法及其适用场景:
1. 使用 Set 数据结构
原理
- Redis 的 Set 是无序且不重复的字符串集合,天然适合存储唯一用户标识(如用户ID、Cookie ID)。
- 通过
SADD
添加用户标识,SCARD
获取集合大小即可得到 UV 值。
优点
- 精确统计:结果完全准确,适合对精度要求高的场景。
- 简单直观:实现逻辑清晰,易于理解。
缺点
- 内存占用高:每个用户ID需要存储为字符串,若用户量极大(如数百万级),内存消耗显著。
- 性能瓶颈:大规模数据下,Set 的读写性能可能下降。
适用场景
- 小规模用户统计(如内部系统、低流量页面)。
- 需要精确 UV 值且用户量可控的场景。
代码示例
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 添加用户访问记录
def add_uv(user_id):
r.sadd('uv_set:2025-07-18', user_id)
# 获取 UV 数量
def get_uv():
return r.scard('uv_set:2025-07-18')
2. 使用 HyperLogLog
原理
- HyperLogLog 是一种概率算法,通过哈希运算估算集合基数(即唯一元素数量),内存占用极低(固定 12KB)。
- 通过
PFADD
添加用户标识,PFCOUNT
获取估算值。
优点
- 内存高效:仅需 12KB 内存即可统计上亿级别 UV。
- 支持合并:可通过
PFMERGE
合并多个 HyperLogLog 数据(如多天数据)。 - 高并发友好:适合海量用户场景。
缺点
- 存在误差:默认标准误差约为 0.81%,不适合对精度要求极高的场景。
- 不可查询具体用户:无法获取具体的用户标识。
适用场景
- 大型网站的 UV 统计(如电商首页、社交平台)。
- 允许一定误差(如 1% 以内)且用户量巨大的场景。
代码示例
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 添加用户访问记录
def add_uv(user_id):
r.pfadd('uv_hll:2025-07-18', user_id)
# 获取 UV 估算值
def get_uv():
return r.pfcount('uv_hll:2025-07-18')
# 合并多天数据(如统计整月 UV)
def merge_month_uv():
r.pfmerge('uv_hll:2025-07', 'uv_hll:2025-07-01', 'uv_hll:2025-07-02', ...)
3. 使用 Bitset(位图)
原理
- 将用户ID映射到位图的偏移量(bit 位),通过
SETBIT
标记用户访问,BITCOUNT
统计访问人数。 - 若用户ID是连续的整数(如用户自增ID),可直接使用;否则需通过哈希算法将非连续ID转换为偏移量。
优点
- 内存压缩:1 个 bit 表示一个用户访问,1 亿用户仅需 12MB 内存。
- 支持精确查询:可通过
GETBIT
判断特定用户是否访问过。
缺点
- 依赖用户ID连续性:若用户ID稀疏,内存浪费严重。
- 哈希冲突风险:非连续ID时,不同用户可能映射到同一偏移量(需额外处理)。
适用场景
- 用户ID为连续整数的场景(如数据库自增ID)。
- 需要精确查询单个用户是否访问过(如会员签到功能)。
代码示例
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 添加用户访问记录(假设用户ID为连续整数)
def add_uv(user_id):
r.setbit('uv_bitmap:2025-07-18', user_id, 1)
# 获取 UV 数量
def get_uv():
return r.bitcount('uv_bitmap:2025-07-18')
4. 结合布隆过滤器(Bloom Filter)优化
原理
- 布隆过滤器 是一种快速判断元素是否存在的概率型数据结构,可减少对 Redis 的无效写入。
- 在写入 Set 或 HyperLogLog 之前,先通过布隆过滤器判断用户是否已存在,避免重复操作。
优点
- 减少 Redis 写压力:避免大量无效的
SADD
或PFADD
操作。 - 提升性能:布隆过滤器基于内存操作,速度极快。
缺点
- 误判率:存在一定的误判概率(需合理设置参数)。
- 无法删除元素:布隆过滤器不支持删除操作(需使用 Counting Bloom Filter)。
适用场景
- 高并发、高频访问的场景(如秒杀活动、实时推荐)。
- 需要降低 Redis 写入负载的场景。
代码示例
from bloomfilter import BloomFilter
import redis
bloom = BloomFilter(max_elements=1000000, error_rate=0.01)
r = redis.Redis(host='localhost', port=6379, db=0)
# 添加用户访问记录
def add_uv(user_id):
if user_id not in bloom:
r.sadd('uv_set:2025-07-18', user_id)
bloom.add(user_id)
# 获取 UV 数量
def get_uv():
return r.scard('uv_set:2025-07-18')
5. 分布式环境下的 UV 统计
挑战
- 多节点部署时,需确保所有节点的 UV 数据能够全局汇总。
解决方案
- 使用 HyperLogLog:各节点独立维护 HLL,最终通过
PFMERGE
合并。 - 使用 Set 或 Bitset:各节点写入独立的 Set/Bitset,通过
SUNION
或BITOP
合并。
代码示例(HyperLogLog 合并)
# 节点A和节点B独立记录 UV
# 节点A
r_a.pfadd('uv_hll:A', user_id1, user_id2)
# 节点B
r_b.pfadd('uv_hll:B', user_id3, user_id4)
# 合并所有节点的 UV
r_master.pfmerge('uv_hll:total', 'uv_hll:A', 'uv_hll:B')
uv_total = r_master.pfcount('uv_hll:total')
总结与选择建议
方法 | 内存占用 | 精度 | 适用场景 | 推荐指数 |
---|---|---|---|---|
Set | 高 | 精确 | 小规模、精确统计 | ⭐⭐⭐⭐ |
HyperLogLog | 极低 | 近似 | 大规模、允许误差 | ⭐⭐⭐⭐⭐ |
Bitset | 中等 | 精确 | 用户ID连续、需查询单用户 | ⭐⭐⭐ |
布隆过滤器优化 | 极低 | 近似 | 高并发、降低写入压力 | ⭐⭐⭐⭐ |
选择策略
- 数据量小且需要精确值 → 使用 Set。
- 数据量极大且允许误差 → 使用 HyperLogLog。
- 用户ID连续且需查询单用户 → 使用 Bitset。
- 高并发、需降低 Redis 写压力 → 结合 布隆过滤器 + Set/HLL。
通过合理选择方法,Redis 可高效应对 UV 统计的性能与资源需求。
THE END