如何使用 Redis 完成 PV,UV 统计?

开发
本文我们分析了如何使用 Redis 统计 PV 和 UV,通过 Redis 的 INCR​ 和 HyperLogLog 数据结构,可以高效地实现 PV 和 UV 的统计。

面试中,我们经常会被问题 PV,UV,那么,什么是 PV?什么又是UV?如何使用 Redis 统计 PV 和 UV?这篇文章,我们将详细介绍如何在 Java 中使用 Redis 实现 PV 和 UV 的统计。

1. 什么是 PV 和 UV?

  • PV(Page Views):指页面被访问的总次数。每一次页面加载或刷新都会增加一次 PV,无论访问者是谁。
  • UV(Unique Visitors):指独立访客数。通常通过用户的唯一标识(如用户 ID、IP 地址、Cookie 等)来统计同一用户在一定时间范围内的访问次数,确保每个独立访客只计数一次。

2. Redis 如何统计 PV 和 UV?

(1) 统计 PV

统计 PV 可以通过 Redis 的 INCR 命令实现。这是一个原子操作,可以确保在高并发情况下准确计数。

(2) 统计 UV

统计 UV 可以使用 Redis 的 HyperLogLog 或 Bitmap 数据结构:

  • HyperLogLog:适合大规模去重统计,占用内存小,但只能估算基数,误差约为 0.81%。
  • Bitmap:通过位图记录用户访问情况,适合用户 ID 范围固定且不大的场景。

本示例中将使用 HyperLogLog 来统计 UV,因为它适用于大规模和动态用户场景,且实现简单。

(3) 数据结构设计

假设我们要统计某个页面(例如 /home)每日的 PV 和 UV,可以设计如下 Redis 键:

  • pv:home:20250301 — 存储 /home 页面在 2025年3月1日的 PV 计数。
  • uv:home:20250301 — 存储 /home 页面在 2025年3月1日的 UV 计数。

3. 示例代码

为了更好地理解如何使用 Redis统计 PV,UV,确保在项目中添加 Jedis 依赖。

(1) 示例代码:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

publicclass RedisPvUvCounter {
    // Redis 服务器配置
    privatestaticfinal String REDIS_HOST = "localhost";
    privatestaticfinalint REDIS_PORT = 6379;
    privatestaticfinal String PAGE_NAME = "home"; // 页面名称
    privatestaticfinal DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    private JedisPool jedisPool;

    // 构造方法,初始化 Jedis 连接池
    public RedisPvUvCounter() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(128); // 最大连接数,可根据需要调整
        this.jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
    }

    /**
     * 统计 PV
     * @param pageName 页面名称
     */
    public void incrementPv(String pageName) {
        String date = LocalDate.now().format(DATE_FORMATTER);
        String pvKey = String.format("pv:%s:%s", pageName, date);
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.incr(pvKey);
        }
    }

    /**
     * 统计 UV
     * @param pageName 页面名称
     * @param userId   用户唯一标识
     */
    public void addUv(String pageName, String userId) {
        String date = LocalDate.now().format(DATE_FORMATTER);
        String uvKey = String.format("uv:%s:%s", pageName, date);
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.pfadd(uvKey, userId);
        }
    }

    /**
     * 获取 PV 统计
     * @param pageName 页面名称
     * @return PV 数量
     */
    public long getPv(String pageName) {
        String date = LocalDate.now().format(DATE_FORMATTER);
        String pvKey = String.format("pv:%s:%s", pageName, date);
        try (Jedis jedis = jedisPool.getResource()) {
            String pvStr = jedis.get(pvKey);
            return pvStr != null ? Long.parseLong(pvStr) : 0;
        }
    }

    /**
     * 获取 UV 统计
     * @param pageName 页面名称
     * @return UV 数量
     */
    public long getUv(String pageName) {
        String date = LocalDate.now().format(DATE_FORMATTER);
        String uvKey = String.format("uv:%s:%s", pageName, date);
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.pfcount(uvKey);
        }
    }

    /**
     * 设置键的过期时间(例如 2 天后过期)
     * @param key キー
     * @param seconds 秒数
     */
    public void setExpire(String key, int seconds) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.expire(key, seconds);
        }
    }

    /**
     * 关闭 Jedis 连接池
     */
    public void close() {
        if (jedisPool != null) {
            jedisPool.close();
        }
    }

    public static void main(String[] args) {
        RedisPvUvCounter counter = new RedisPvUvCounter();

        String page = "home";
        String user1 = "user_001";
        String user2 = "user_002";

        // 模拟 PV 和 UV 统计
        counter.incrementPv(page);
        counter.addUv(page, user1);

        counter.incrementPv(page);
        counter.addUv(page, user1); // 重复访问,不增加 UV

        counter.incrementPv(page);
        counter.addUv(page, user2);

        // 设置键的过期时间(可选,根据实际需求)
        String date = LocalDate.now().format(DATE_FORMATTER);
        String pvKey = String.format("pv:%s:%s", page, date);
        String uvKey = String.format("uv:%s:%s", page, date);
        counter.setExpire(pvKey, 2 * 24 * 60 * 60); // PV 键 2 天后过期
        counter.setExpire(uvKey, 2 * 24 * 60 * 60); // UV 键 2 天后过期

        // 获取统计结果
        long pv = counter.getPv(page);
        long uv = counter.getUv(page);

        System.out.println("PV 总数: " + pv); // 输出: PV 总数: 3
        System.out.println("UV 总数: " + uv); // 输出: UV 总数: 2

        // 关闭连接池
        counter.close();
    }
}

(2) 代码详解

①连接 Redis

使用 JedisPool 来管理 Redis 连接池,提升性能和资源利用率。通过配置 JedisPoolConfig 可以调整连接池的相关参数,如最大连接数等。

②统计 PV

  • 使用 INCR 命令对 PV 键进行自增。
  • 键的命名规范为 pv:{pageName}:{date}(例如 pv:home:20250301)。
  • 每访问一次页面,调用 incrementPv 方法即可增加 PV 计数。

③统计 UV

  • 使用 PFADD 命令将用户的唯一标识添加到 HyperLogLog 结构中。
  • 键的命名规范为 uv:{pageName}:{date}(例如 uv:home:20250301)。
  • userId 可以是用户的登录 ID、IP 地址或其他唯一标识。
  • HyperLogLog 会自动去重,因此即使同一个用户多次访问,也只会计数一次。

④获取 PV 和 UV 数量

  • PV 使用 GET 命令获取键的值,并转换为 long 类型。如果键不存在,则返回 0。
  • UV 使用 PFCOUNT 命令获取 HyperLogLog 的估算基数。

⑤设置键的过期时间

为了避免 Redis 中存储过多历史数据,可以为 PV 和 UV 键设置过期时间。本示例中设置为 2 天后过期。可以根据实际需求调整。

⑥关闭连接池

使用完毕后,调用 close 方法关闭 JedisPool,释放资源。

(3) 运行示例

运行 main 方法后,将模拟以下操作:

  • 用户 user_001 访问 /home 页面,PV 增加 1,UV 增加 1。
  • 用户 user_001 再次访问 /home 页面,PV 增加 1,UV 不变。
  • 用户 user_002 访问 /home 页面,PV 增加 1,UV 增加 1。

最终输出:

PV 总数: 3
UV 总数: 2

4. 扩展与优化

(1) 设置键的过期时间

可以在 incrementPv 和 addUv 方法中设置键的过期时间,以自动删除过期数据,避免 Redis 内存不断增长。

public void incrementPv(String pageName) {
    String date = LocalDate.now().format(DATE_FORMATTER);
    String pvKey = String.format("pv:%s:%s", pageName, date);
    try (Jedis jedis = jedisPool.getResource()) {
        jedis.incr(pvKey);
        jedis.expire(pvKey, 2 * 24 * 60 * 60); // 设置过期时间为2天
    }
}

public void addUv(String pageName, String userId) {
    String date = LocalDate.now().format(DATE_FORMATTER);
    String uvKey = String.format("uv:%s:%s", pageName, date);
    try (Jedis jedis = jedisPool.getResource()) {
        jedis.pfadd(uvKey, userId);
        jedis.expire(uvKey, 2 * 24 * 60 * 60); // 设置过期时间为2天
    }
}

(2) 使用 Lua 脚本优化

为了减少 Redis 交互次数,可以使用 Lua 脚本将多个命令合并为一个原子操作。例如,可以在一次 Lua 脚本中同时对 PV 和 UV 进行操作。

(3) 分布式环境下的 Redis 集群

在分布式系统中,可以使用 Redis 集群来提高可用性和扩展性。Jedis 提供了 JedisCluster 类来支持 Redis 集群。

(4) 选择合适的唯一标识

为了准确统计 UV,选择唯一标识非常关键。常见的方式包括:

  • 用户登录 ID:最可靠,但仅适用于已认证用户。
  • IP 地址:简单但可能不够准确,受 NAT 和代理影响。
  • Cookie:通过生成唯一的 Cookie 标识符,即使用户未登录也可以追踪。

根据业务需求选择合适的方式,并注意隐私和数据保护。

(5) 持久化与备份

确保 Redis 的持久化机制(RDB 或 AOF)已正确配置,以防止数据丢失。

5. 总结

本文,我们分析了如何使用 Redis 统计 PV 和 UV,通过 Redis 的 INCR 和 HyperLogLog 数据结构,可以高效地实现 PV 和 UV 的统计。另外,实际工作中,我们可以根据实际业务需求,可以进一步优化和扩展,如设置键过期时间、使用 Lua 脚本、部署 Redis 集群等。

责任编辑:赵宁宁 来源: 猿java
相关推荐

2019-10-17 09:25:56

Spark StreaPVUV

2021-08-08 22:08:41

Redis开发网页

2016-10-16 13:48:54

多维分析 UVPV

2021-11-01 13:11:45

FlinkPvUv

2021-06-03 08:10:30

SparkStream项目Uv

2021-06-06 13:10:12

FlinkPvUv

2015-12-23 17:08:25

H5

2015-12-14 14:26:56

Linux命令pv

2023-03-08 08:13:33

Pv工具

2025-02-13 11:11:53

Redis哨兵代码

2024-10-06 12:50:25

2017-12-22 10:34:02

大数据AI存储

2017-12-18 09:02:42

Red Hat SysAnsible集成

2018-12-05 09:00:00

RedisRedis Strea数据库

2021-08-04 17:55:38

keysRedis数据库

2025-03-03 10:25:10

2012-03-28 14:06:43

软件系统系统测试

2024-12-17 15:39:33

2024-03-22 12:10:39

Redis消息队列数据库

2017-05-31 14:55:19

点赞
收藏

51CTO技术栈公众号