当前位置:首页 > 问答 > 正文

用Redis咋快速搞定网站UV和PV统计,效率杠杠的那种

PV统计:简单粗暴,就用计数器

PV统计很简单,就是页面被打开一次就加一,不用管是谁打开的,这在Redis里是最简单的操作。

用Redis咋快速搞定网站UV和PV统计,效率杠杠的那种

  • 基础玩法:用 INCR 命令 对于每个页面,比如你的首页 https://www.example.com/index.html,你可以给它设置一个键(key),每次有用户访问这个页面,就让Redis对这个键执行一次 INCR 命令。

    • 命令示例INCR page:pv:https://www.example.com/index.html
    • 解释INCR 命令会将键对应的值增加1,如果键不存在,会先初始化为0再增加1,这个操作是原子性的,也就是说即使成千上万的请求同时来执行这个命令,Redis也会确保每个都正确计数,不会出错。
    • 查看结果:用 GET page:pv:https://www.example.com/index.html 就能直接读出这个页面的总PV。
  • 进阶玩法:按时间维度统计 光有总PV还不够,我们通常需要看每天、每小时的PV,这时可以给键名加上时间戳。

    用Redis咋快速搞定网站UV和PV统计,效率杠杠的那种

    • 日PVINCR page:pv:20240520:index.html (键名里包含了日期 20240520)
    • 时PVINCR page:pv:2024052014:index.html (键名里包含了日期和小时 2024052014) 这样,你想查询某一天或者某一小时的PV,只需要对对应的键执行 GET 命令就行了,这种方法的优点是查询速度极快,缺点是如果页面很多,键的数量会急剧膨胀(页面数 × 时间粒度),需要做好过期时间管理,比如给按小时的键设置两天的过期时间,给按天的键设置一个月的过期时间。

UV统计:核心难点,关键在于去重

UV统计的是独立用户数,同一个用户一天内访问多次只算一次,这个“去重”是UV统计的难点和关键,用关系型数据库做DISTINCT查询,数据量一大就会非常慢,Redis提供了几种高效的数据结构来解决这个问题。

用Redis咋快速搞定网站UV和PV统计,效率杠杠的那种

  • 使用 Set 集合 Set 是Redis里一种无序但元素唯一的数据结构,正好符合“去重”的需求。

    • 操作:对于同一个页面,比如首页,我们为每一天创建一个Set,键名可以是 page:uv:20240520:index.html,每当有一个用户访问,我们就把这个用户的唯一标识(比如用户ID,或者更常见的,经过哈希计算后的客户端IP地址)SADD(Set Add)到这个集合里。
    • 命令示例SADD page:uv:20240520:index.html user_ip_hash_or_id
    • 获取UV:通过 SCARD page:uv:20240520:index.html 命令,可以立刻获取这个集合的基数(Cardinality),也就是当天的UV数。
    • 优缺点
      • 优点:精确,结果100%准确。
      • 缺点:如果网站流量非常大(比如百万、千万级别),存储这个Set会占用非常大的内存,因为你要存储每一个唯一用户的标识,成本会很高。
  • 使用 HyperLogLog(推荐方案) 这是Redis提供的“大杀器”,专门用于解决海量数据下的唯一计数问题,它的原理有点复杂,但你可以简单理解为:它用极小的、固定大小的内存空间(每个HyperLogLog键只需要约12KB内存),就能统计巨大量数据的基数,并且误差率可以控制在1%以内。

    • 操作:和Set类似,为每个页面和每一天创建一个HyperLogLog结构,键名如 page:uv:hll:20240520:index.html,用户访问时,用 PFADD 命令添加用户标识。
    • 命令示例PFADD page:uv:hll:20240520:index.html user_ip_hash_or_id
    • 获取UV:使用 PFCOUNT page:uv:hll:20240520:index.html 命令,就能得到估算出的UV值。
    • 合并统计:HyperLogLog还有一个强大功能是合并,比如你想统计一周的UV,可以用 PFMERGE 命令把周一到周日七个HyperLogLog合并成一个,然后统计总数,这比用Set做并集计算要高效和节省内存得多。
    • 优缺点
      • 优点:内存占用极小且固定,非常适合超大规模数据的UV统计。
      • 缺点:存在约1%的误差,不是精确值,但对于绝大多数需要看趋势、看大盘的UV统计场景来说,这个精度是完全可接受的。这是目前最主流、最高效的UV统计方案。
  • 使用 Bitmap(位图) Bitmap可以理解为一种更极致的、通过位运算来节省空间的方案,它把每一个用户ID映射到一个比特位(bit)上,比如用户ID是1,就把第1位设为1;用户ID是1000,就把第1000位设为1,最后统计有多少位被设置为1,就是UV。

    • 操作:假设我们用自增的数字ID作为用户标识,键名如 page:uv:bitmap:20240520:index.html,用户访问时,使用 SETBIT 命令。
    • 命令示例SETBIT page:uv:bitmap:20240520:index.html user_id 1 (将第 user_id 位设置为1)
    • 获取UV:使用 BITCOUNT page:uv:bitmap:20240520:index.html 命令统计被设置为1的位的数量。
    • 优缺点
      • 优点:如果用户ID是连续的数字,它的内存效率会非常高(最多可节省32倍空间 compared to Set),统计速度也很快。
      • 缺点:如果用户ID分布非常稀疏(比如用户ID是从1直接跳到1亿),那么内存会有浪费,而且它强依赖于连续的数字ID,如果用IP地址等非数字标识,需要先做一次映射转换,增加了复杂度。

实战流程和优化建议

  1. 数据采集:在网站的每个页面埋点,当页面被访问时,后端服务或前端脚本异步向你的统计服务发起一个请求,这个请求至少携带 页面URL用户标识(如IP的哈希值)。
  2. 异步处理:统计服务接收到请求后,不要同步等待Redis操作完成,而是将其放入一个消息队列(比如Redis自身的List结构做简单队列,或者Kafka等),然后立即返回响应给前端,再由后台的工作进程从队列中消费消息,批量地向Redis执行 INCRPFADD 等命令,这样做的好处是避免统计逻辑阻塞主业务,即使Redis暂时抖动,数据也不会丢失。
  3. 选择数据结构
    • PV:无脑用 INCR
    • UV:在追求精确且数据量可控(例如日UV在百万以内)时用 Set;在数据量巨大且可接受微小误差时,强烈推荐 HyperLogLog;如果用户体系本身就是连续数字ID且对内存有极致要求,可以考虑 Bitmap
  4. 键名设计和过期:键名要有清晰的命名空间,如 pv:日期:页面uv:hll:日期:页面,一定要为这些键设置过期时间(TTL),比如30天或90天,避免无用数据永远占用内存,使用 EXPIRE 命令设置。
  5. 数据持久化与归档:Redis数据主要在内存里,虽然有持久化机制,但通常我们只把它当作一个高速计算层,你需要定期(比如每天凌晨)将前一天的PV和UV结果从Redis里查询出来,保存到MySQL或数据仓库(如ClickHouse)中,用于做历史数据的复杂分析和报表展示,清空或等待Redis中的过期键自动删除。

用Redis搞UV/PV统计,PV靠INCR,UV首选HyperLogLog,配合异步处理和合理的键管理,就能搭建一个效率极高、扩展性极强的实时统计系统,轻松应对海量访问。