如何使用 Redis 完成 PV,UV 統(tǒng)計(jì)?
面試中,我們經(jīng)常會(huì)被問題 PV,UV,那么,什么是 PV?什么又是UV?如何使用 Redis 統(tǒng)計(jì) PV 和 UV?這篇文章,我們將詳細(xì)介紹如何在 Java 中使用 Redis 實(shí)現(xiàn) PV 和 UV 的統(tǒng)計(jì)。

1. 什么是 PV 和 UV?
- PV(Page Views):指頁面被訪問的總次數(shù)。每一次頁面加載或刷新都會(huì)增加一次 PV,無論訪問者是誰。
- UV(Unique Visitors):指獨(dú)立訪客數(shù)。通常通過用戶的唯一標(biāo)識(shí)(如用戶 ID、IP 地址、Cookie 等)來統(tǒng)計(jì)同一用戶在一定時(shí)間范圍內(nèi)的訪問次數(shù),確保每個(gè)獨(dú)立訪客只計(jì)數(shù)一次。
2. Redis 如何統(tǒng)計(jì) PV 和 UV?
(1) 統(tǒng)計(jì) PV
統(tǒng)計(jì) PV 可以通過 Redis 的 INCR 命令實(shí)現(xiàn)。這是一個(gè)原子操作,可以確保在高并發(fā)情況下準(zhǔn)確計(jì)數(shù)。
(2) 統(tǒng)計(jì) UV
統(tǒng)計(jì) UV 可以使用 Redis 的 HyperLogLog 或 Bitmap 數(shù)據(jù)結(jié)構(gòu):
- HyperLogLog:適合大規(guī)模去重統(tǒng)計(jì),占用內(nèi)存小,但只能估算基數(shù),誤差約為 0.81%。
- Bitmap:通過位圖記錄用戶訪問情況,適合用戶 ID 范圍固定且不大的場(chǎng)景。
本示例中將使用 HyperLogLog 來統(tǒng)計(jì) UV,因?yàn)樗m用于大規(guī)模和動(dòng)態(tài)用戶場(chǎng)景,且實(shí)現(xiàn)簡單。
(3) 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
假設(shè)我們要統(tǒng)計(jì)某個(gè)頁面(例如 /home)每日的 PV 和 UV,可以設(shè)計(jì)如下 Redis 鍵:
- pv:home:20250301 — 存儲(chǔ) /home 頁面在 2025年3月1日的 PV 計(jì)數(shù)。
- uv:home:20250301 — 存儲(chǔ) /home 頁面在 2025年3月1日的 UV 計(jì)數(shù)。
3. 示例代碼
為了更好地理解如何使用 Redis統(tǒng)計(jì) PV,UV,確保在項(xiàng)目中添加 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 服務(wù)器配置
privatestaticfinal String REDIS_HOST = "localhost";
privatestaticfinalint REDIS_PORT = 6379;
privatestaticfinal String PAGE_NAME = "home"; // 頁面名稱
privatestaticfinal DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private JedisPool jedisPool;
// 構(gòu)造方法,初始化 Jedis 連接池
public RedisPvUvCounter() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128); // 最大連接數(shù),可根據(jù)需要調(diào)整
this.jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
}
/**
* 統(tǒng)計(jì) 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);
}
}
/**
* 統(tǒng)計(jì) UV
* @param pageName 頁面名稱
* @param userId 用戶唯一標(biāo)識(shí)
*/
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 統(tǒng)計(jì)
* @param pageName 頁面名稱
* @return PV 數(shù)量
*/
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 統(tǒng)計(jì)
* @param pageName 頁面名稱
* @return UV 數(shù)量
*/
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);
}
}
/**
* 設(shè)置鍵的過期時(shí)間(例如 2 天后過期)
* @param key キー
* @param seconds 秒數(shù)
*/
public void setExpire(String key, int seconds) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.expire(key, seconds);
}
}
/**
* 關(guān)閉 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 統(tǒng)計(jì)
counter.incrementPv(page);
counter.addUv(page, user1);
counter.incrementPv(page);
counter.addUv(page, user1); // 重復(fù)訪問,不增加 UV
counter.incrementPv(page);
counter.addUv(page, user2);
// 設(shè)置鍵的過期時(shí)間(可選,根據(jù)實(shí)際需求)
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 天后過期
// 獲取統(tǒng)計(jì)結(jié)果
long pv = counter.getPv(page);
long uv = counter.getUv(page);
System.out.println("PV 總數(shù): " + pv); // 輸出: PV 總數(shù): 3
System.out.println("UV 總數(shù): " + uv); // 輸出: UV 總數(shù): 2
// 關(guān)閉連接池
counter.close();
}
}(2) 代碼詳解
①連接 Redis
使用 JedisPool 來管理 Redis 連接池,提升性能和資源利用率。通過配置 JedisPoolConfig 可以調(diào)整連接池的相關(guān)參數(shù),如最大連接數(shù)等。
②統(tǒng)計(jì) PV
- 使用 INCR 命令對(duì) PV 鍵進(jìn)行自增。
- 鍵的命名規(guī)范為 pv:{pageName}:{date}(例如 pv:home:20250301)。
- 每訪問一次頁面,調(diào)用 incrementPv 方法即可增加 PV 計(jì)數(shù)。
③統(tǒng)計(jì) UV
- 使用 PFADD 命令將用戶的唯一標(biāo)識(shí)添加到 HyperLogLog 結(jié)構(gòu)中。
- 鍵的命名規(guī)范為 uv:{pageName}:{date}(例如 uv:home:20250301)。
- userId 可以是用戶的登錄 ID、IP 地址或其他唯一標(biāo)識(shí)。
- HyperLogLog 會(huì)自動(dòng)去重,因此即使同一個(gè)用戶多次訪問,也只會(huì)計(jì)數(shù)一次。
④獲取 PV 和 UV 數(shù)量
- PV 使用 GET 命令獲取鍵的值,并轉(zhuǎn)換為 long 類型。如果鍵不存在,則返回 0。
- UV 使用 PFCOUNT 命令獲取 HyperLogLog 的估算基數(shù)。
⑤設(shè)置鍵的過期時(shí)間
為了避免 Redis 中存儲(chǔ)過多歷史數(shù)據(jù),可以為 PV 和 UV 鍵設(shè)置過期時(shí)間。本示例中設(shè)置為 2 天后過期??梢愿鶕?jù)實(shí)際需求調(diào)整。
⑥關(guān)閉連接池
使用完畢后,調(diào)用 close 方法關(guān)閉 JedisPool,釋放資源。
(3) 運(yùn)行示例
運(yùn)行 main 方法后,將模擬以下操作:
- 用戶 user_001 訪問 /home 頁面,PV 增加 1,UV 增加 1。
- 用戶 user_001 再次訪問 /home 頁面,PV 增加 1,UV 不變。
- 用戶 user_002 訪問 /home 頁面,PV 增加 1,UV 增加 1。
最終輸出:
PV 總數(shù): 3
UV 總數(shù): 24. 擴(kuò)展與優(yōu)化
(1) 設(shè)置鍵的過期時(shí)間
可以在 incrementPv 和 addUv 方法中設(shè)置鍵的過期時(shí)間,以自動(dòng)刪除過期數(shù)據(jù),避免 Redis 內(nèi)存不斷增長。
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); // 設(shè)置過期時(shí)間為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); // 設(shè)置過期時(shí)間為2天
}
}(2) 使用 Lua 腳本優(yōu)化
為了減少 Redis 交互次數(shù),可以使用 Lua 腳本將多個(gè)命令合并為一個(gè)原子操作。例如,可以在一次 Lua 腳本中同時(shí)對(duì) PV 和 UV 進(jìn)行操作。
(3) 分布式環(huán)境下的 Redis 集群
在分布式系統(tǒng)中,可以使用 Redis 集群來提高可用性和擴(kuò)展性。Jedis 提供了 JedisCluster 類來支持 Redis 集群。
(4) 選擇合適的唯一標(biāo)識(shí)
為了準(zhǔn)確統(tǒng)計(jì) UV,選擇唯一標(biāo)識(shí)非常關(guān)鍵。常見的方式包括:
- 用戶登錄 ID:最可靠,但僅適用于已認(rèn)證用戶。
- IP 地址:簡單但可能不夠準(zhǔn)確,受 NAT 和代理影響。
- Cookie:通過生成唯一的 Cookie 標(biāo)識(shí)符,即使用戶未登錄也可以追蹤。
根據(jù)業(yè)務(wù)需求選擇合適的方式,并注意隱私和數(shù)據(jù)保護(hù)。
(5) 持久化與備份
確保 Redis 的持久化機(jī)制(RDB 或 AOF)已正確配置,以防止數(shù)據(jù)丟失。
5. 總結(jié)
本文,我們分析了如何使用 Redis 統(tǒng)計(jì) PV 和 UV,通過 Redis 的 INCR 和 HyperLogLog 數(shù)據(jù)結(jié)構(gòu),可以高效地實(shí)現(xiàn) PV 和 UV 的統(tǒng)計(jì)。另外,實(shí)際工作中,我們可以根據(jù)實(shí)際業(yè)務(wù)需求,可以進(jìn)一步優(yōu)化和擴(kuò)展,如設(shè)置鍵過期時(shí)間、使用 Lua 腳本、部署 Redis 集群等。






























