用Redis咋快速搞定网站UV和PV统计,效率杠杠的那种
- 问答
- 2025-12-28 12:32:51
- 3
PV统计:简单粗暴,就用计数器
PV统计很简单,就是页面被打开一次就加一,不用管是谁打开的,这在Redis里是最简单的操作。

-
基础玩法:用
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,这时可以给键名加上时间戳。

- 日PV:
INCR page:pv:20240520:index.html(键名里包含了日期 20240520) - 时PV:
INCR page:pv:2024052014:index.html(键名里包含了日期和小时 2024052014) 这样,你想查询某一天或者某一小时的PV,只需要对对应的键执行GET命令就行了,这种方法的优点是查询速度极快,缺点是如果页面很多,键的数量会急剧膨胀(页面数 × 时间粒度),需要做好过期时间管理,比如给按小时的键设置两天的过期时间,给按天的键设置一个月的过期时间。
- 日PV:
UV统计:核心难点,关键在于去重
UV统计的是独立用户数,同一个用户一天内访问多次只算一次,这个“去重”是UV统计的难点和关键,用关系型数据库做DISTINCT查询,数据量一大就会非常慢,Redis提供了几种高效的数据结构来解决这个问题。

-
使用 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会占用非常大的内存,因为你要存储每一个唯一用户的标识,成本会很高。
- 操作:对于同一个页面,比如首页,我们为每一天创建一个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统计方案。
- 操作:和Set类似,为每个页面和每一天创建一个HyperLogLog结构,键名如
-
使用 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地址等非数字标识,需要先做一次映射转换,增加了复杂度。
- 操作:假设我们用自增的数字ID作为用户标识,键名如
实战流程和优化建议
- 数据采集:在网站的每个页面埋点,当页面被访问时,后端服务或前端脚本异步向你的统计服务发起一个请求,这个请求至少携带
页面URL和用户标识(如IP的哈希值)。 - 异步处理:统计服务接收到请求后,不要同步等待Redis操作完成,而是将其放入一个消息队列(比如Redis自身的List结构做简单队列,或者Kafka等),然后立即返回响应给前端,再由后台的工作进程从队列中消费消息,批量地向Redis执行
INCR、PFADD等命令,这样做的好处是避免统计逻辑阻塞主业务,即使Redis暂时抖动,数据也不会丢失。 - 选择数据结构:
- PV:无脑用
INCR。 - UV:在追求精确且数据量可控(例如日UV在百万以内)时用 Set;在数据量巨大且可接受微小误差时,强烈推荐 HyperLogLog;如果用户体系本身就是连续数字ID且对内存有极致要求,可以考虑 Bitmap。
- PV:无脑用
- 键名设计和过期:键名要有清晰的命名空间,如
pv:日期:页面、uv:hll:日期:页面,一定要为这些键设置过期时间(TTL),比如30天或90天,避免无用数据永远占用内存,使用EXPIRE命令设置。 - 数据持久化与归档:Redis数据主要在内存里,虽然有持久化机制,但通常我们只把它当作一个高速计算层,你需要定期(比如每天凌晨)将前一天的PV和UV结果从Redis里查询出来,保存到MySQL或数据仓库(如ClickHouse)中,用于做历史数据的复杂分析和报表展示,清空或等待Redis中的过期键自动删除。
用Redis搞UV/PV统计,PV靠INCR,UV首选HyperLogLog,配合异步处理和合理的键管理,就能搭建一个效率极高、扩展性极强的实时统计系统,轻松应对海量访问。
本文由寇乐童于2025-12-28发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/70031.html
