面试题:如何使用 Redis 统计大量用户唯一访问量(UV)?

在互联网应用中,统计大量用户的唯一访问量(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连续、需查询单用户⭐⭐⭐
布隆过滤器优化极低近似高并发、降低写入压力⭐⭐⭐⭐

选择策略

  1. 数据量小且需要精确值 → 使用 Set
  2. 数据量极大且允许误差 → 使用 HyperLogLog
  3. 用户ID连续且需查询单用户 → 使用 Bitset
  4. 高并发、需降低 Redis 写压力 → 结合 布隆过滤器 + Set/HLL

通过合理选择方法,Redis 可高效应对 UV 统计的性能与资源需求。

THE END
喜欢就支持一下吧
点赞10 分享