你好,我是猿java。
面试中,我们经常会被问题 PV,UV,那么,什么是 PV?什么又是UV?如何使用 Redis 统计 PV 和 UV?这篇文章,我们将详细介绍如何在 Java 中使用 Redis 实现 PV 和 UV 的统计。
1. 什么是 PV 和 UV?
2. Redis 如何统计 PV 和 UV?
2.1 统计 PV
统计 PV 可以通过 Redis 的 INCR
命令实现。这是一个原子操作,可以确保在高并发情况下准确计数。
2.2 统计 UV
统计 UV 可以使用 Redis 的 HyperLogLog
或 Bitmap
数据结构:
- HyperLogLog:适合大规模去重统计,占用内存小,但只能估算基数,误差约为 0.81%。
- Bitmap:通过位图记录用户访问情况,适合用户 ID 范围固定且不大的场景。
本示例中将使用 HyperLogLog
来统计 UV,因为它适用于大规模和动态用户场景,且实现简单。
2.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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig;
import java.time.LocalDate; import java.time.format.DateTimeFormatter;
public class RedisPvUvCounter { private static final String REDIS_HOST = "localhost"; private static final int REDIS_PORT = 6379; private static final String PAGE_NAME = "home"; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private JedisPool jedisPool;
public RedisPvUvCounter() { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(128); this.jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT); }
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); } }
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); } }
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; } }
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); } }
public void setExpire(String key, int seconds) { try (Jedis jedis = jedisPool.getResource()) { jedis.expire(key, seconds); } }
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";
counter.incrementPv(page); counter.addUv(page, user1);
counter.incrementPv(page); counter.addUv(page, user1);
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); counter.setExpire(uvKey, 2 * 24 * 60 * 60);
long pv = counter.getPv(page); long uv = counter.getUv(page);
System.out.println("PV 总数: " + pv); System.out.println("UV 总数: " + uv);
counter.close(); } }
|
代码详解
连接 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
,释放资源。
运行示例
运行 main
方法后,将模拟以下操作:
- 用户
user_001
访问 /home
页面,PV 增加 1,UV 增加 1。
- 用户
user_001
再次访问 /home
页面,PV 增加 1,UV 不变。
- 用户
user_002
访问 /home
页面,PV 增加 1,UV 增加 1。
最终输出:
4. 扩展与优化
4.1 设置键的过期时间
可以在 incrementPv
和 addUv
方法中设置键的过期时间,以自动删除过期数据,避免 Redis 内存不断增长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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); } }
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); } }
|
4.2 使用 Lua 脚本优化
为了减少 Redis 交互次数,可以使用 Lua 脚本将多个命令合并为一个原子操作。例如,可以在一次 Lua 脚本中同时对 PV 和 UV 进行操作。
4.3 分布式环境下的 Redis 集群
在分布式系统中,可以使用 Redis 集群来提高可用性和扩展性。Jedis 提供了 JedisCluster
类来支持 Redis 集群。
4.4 选择合适的唯一标识
为了准确统计 UV,选择唯一标识非常关键。常见的方式包括:
- 用户登录 ID:最可靠,但仅适用于已认证用户。
- IP 地址:简单但可能不够准确,受 NAT 和代理影响。
- Cookie:通过生成唯一的 Cookie 标识符,即使用户未登录也可以追踪。
根据业务需求选择合适的方式,并注意隐私和数据保护。
4.5 持久化与备份
确保 Redis 的持久化机制(RDB 或 AOF)已正确配置,以防止数据丢失。
5. 总结
本文,我们分析了如何使用 Redis 统计 PV 和 UV,通过 Redis 的 INCR
和 HyperLogLog
数据结构,可以高效地实现 PV 和 UV 的统计。另外,实际工作中,我们可以根据实际业务需求,可以进一步优化和扩展,如设置键过期时间、使用 Lua 脚本、部署 Redis 集群等。
6. 学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。