Redis HyperLogLog用戶統(tǒng)計(jì)功能實(shí)現(xiàn)代碼
一、 UV 統(tǒng)計(jì)的業(yè)務(wù)訴求
在互聯(lián)網(wǎng)產(chǎn)品運(yùn)營(yíng)體系中,用戶訪問(wèn)量統(tǒng)計(jì)是衡量產(chǎn)品活躍度與流量規(guī)模的核心指標(biāo)。其中涉及兩個(gè)關(guān)鍵概念:
UV(Unique Visitor)獨(dú)立訪客量
指通過(guò)互聯(lián)網(wǎng)訪問(wèn)、瀏覽該網(wǎng)頁(yè)的自然人數(shù)量。1 天內(nèi)同一個(gè)用戶多次訪問(wèn)該網(wǎng)站,僅記錄 1 次。UV 反映了產(chǎn)品的真實(shí)用戶覆蓋范圍。
PV(Page View)頁(yè)面訪問(wèn)量
用戶每訪問(wèn)網(wǎng)站的一個(gè)頁(yè)面,記錄 1 次 PV。用戶多次打開(kāi)同一頁(yè)面,則記錄多次 PV。PV 往往用來(lái)衡量網(wǎng)站的總體流量與用戶粘性。
1.1 傳統(tǒng) UV 統(tǒng)計(jì)方案
UV 統(tǒng)計(jì)在服務(wù)端實(shí)現(xiàn)較為復(fù)雜,核心難點(diǎn)在于去重判斷。系統(tǒng)需要判斷該用戶是否已經(jīng)被統(tǒng)計(jì)過(guò),必須將已統(tǒng)計(jì)過(guò)的用戶信息持久化保存。
若采用傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)方案,需維護(hù)一張 user_visit_log 表,記錄 (user_id, visit_date) 的唯一索引。當(dāng)日活用戶突破百萬(wàn)級(jí)時(shí),該表將產(chǎn)生以下問(wèn)題:
- 存儲(chǔ)空間爆炸:百萬(wàn)級(jí)用戶 × 365 天 = 3.65 億條記錄,索引與數(shù)據(jù)文件占用數(shù)十 GB 磁盤(pán)空間。
- 寫(xiě)入性能劣化:每次用戶訪問(wèn)需執(zhí)行
INSERT IGNORE或ON DUPLICATE KEY UPDATE,B+ 樹(shù)索引頻繁分裂與合并,數(shù)據(jù)庫(kù) CPU 與 IO 負(fù)載居高不下。 - 查詢延遲飆升:統(tǒng)計(jì)某日 UV 需執(zhí)行
SELECT COUNT(DISTINCT user_id) FROM user_visit_log WHERE date = ?,全表掃描與臨時(shí)表排序?qū)е马憫?yīng)時(shí)間呈指數(shù)級(jí)上升。
若采用 Redis Set 結(jié)構(gòu)存儲(chǔ)每日訪問(wèn)用戶 ID,雖可將查詢延遲壓縮至毫秒級(jí),但內(nèi)存消耗依然恐怖。假設(shè)日活用戶 1000 萬(wàn),每個(gè)用戶 ID 占用 8 字節(jié)(Long 類型),單日數(shù)據(jù)即需 80MB 內(nèi)存。全年累計(jì)需 29GB 內(nèi)存,成本高昂且不可持續(xù)。
二、 HyperLogLog 的優(yōu)勢(shì)
2.1 算法原理與核心特性
HyperLogLog(HLL)是從 LogLog 算法派生的概率算法,用于確定非常大的集合的基數(shù)(Cardinality),而不需要存儲(chǔ)其所有值。其核心思想是利用概率統(tǒng)計(jì)與哈希函數(shù)的均勻分布特性,通過(guò)觀察哈希值的二進(jìn)制模式中前導(dǎo)零的數(shù)量,估算集合中不同元素的數(shù)量。
Redis 中的 HLL 是基于 String 結(jié)構(gòu)實(shí)現(xiàn)的,單個(gè) HLL 的內(nèi)存占用永遠(yuǎn)小于 16KB。這一極致的內(nèi)存壓縮比,使得 HyperLogLog 成為海量數(shù)據(jù)去重統(tǒng)計(jì)的不二之選。
代價(jià)與權(quán)衡
作為概率算法,HyperLogLog 的測(cè)量結(jié)果存在小于 0.81% 的標(biāo)準(zhǔn)誤差。但對(duì)于 UV 統(tǒng)計(jì)這類業(yè)務(wù)場(chǎng)景而言,這一誤差完全可以忽略。運(yùn)營(yíng)人員關(guān)注的是流量趨勢(shì)與量級(jí),而非精確到個(gè)位數(shù)的統(tǒng)計(jì)結(jié)果。
2.2 Redis HyperLogLog 核心命令
| 命令 | 功能描述 | 時(shí)間復(fù)雜度 |
|---|---|---|
PFADD key element [element ...] | 添加一個(gè)或多個(gè)元素到 HyperLogLog | O(1) |
PFCOUNT key [key ...] | 計(jì)算一個(gè)或多個(gè) HyperLogLog 的并集基數(shù) | O(N) |
PFMERGE destkey sourcekey [sourcekey ...] | 將多個(gè) HyperLogLog 合并為一個(gè) | O(N) |
命令詳解:
PFADD:向指定 Key 的 HyperLogLog 中添加元素。若元素已存在,不會(huì)重復(fù)計(jì)數(shù)。返回值為 1 表示 HyperLogLog 被修改(新增元素),0 表示元素已存在。PFCOUNT:返回指定 Key 的估算基數(shù)。支持傳入多個(gè) Key,返回它們的并集去重?cái)?shù)量,適用于跨天、跨維度的合并統(tǒng)計(jì)。PFMERGE:將多個(gè)源 HyperLogLog 合并到目標(biāo) Key 中,適用于數(shù)據(jù)歸檔與離線分析。
三、 UV 統(tǒng)計(jì)實(shí)現(xiàn)與壓測(cè)驗(yàn)證
3.1 單元測(cè)試壓測(cè)代碼
我們通過(guò)單元測(cè)試向 HyperLogLog 中添加 100 萬(wàn)條數(shù)據(jù),驗(yàn)證其內(nèi)存占用與統(tǒng)計(jì)精度:
import redis
import time
def test_hyperloglog():
# 連接 Redis
r = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True)
# 清空測(cè)試 Key
r.delete('hll:uv:daily')
# 準(zhǔn)備批量添加
batch_size = 1000
total_users = 1000000
start_time = time.time()
# 批量添加 100 萬(wàn)用戶
for i in range(0, total_users, batch_size):
users = [f"user_{j}" for j in range(i + 1, min(i + batch_size + 1, total_users + 1))]
r.pfadd('hll:uv:daily', *users)
# 統(tǒng)計(jì)數(shù)量
uv_count = r.pfcount('hll:uv:daily')
# 獲取內(nèi)存占用
memory_used = r.memory_usage('hll:uv:daily')
elapsed_time = time.time() - start_time
print(f"實(shí)際添加用戶數(shù): {total_users}")
print(f"HyperLogLog 統(tǒng)計(jì)結(jié)果: {uv_count}")
print(f"誤差率: {abs(uv_count - total_users) / total_users * 100:.4f}%")
print(f"內(nèi)存占用: {memory_used} bytes ({memory_used / 1024:.2f} KB)")
print(f"耗時(shí): {elapsed_time:.4f} 秒")
print(f"吞吐量: {total_users / elapsed_time:.0f} ops/sec")
if __name__ == "__main__":
test_hyperloglog()壓測(cè)結(jié)果分析(典型輸出):
實(shí)際添加用戶數(shù): 1000000 HyperLogLog 統(tǒng)計(jì)結(jié)果: 1008542 誤差率: 0.8542% 內(nèi)存占用: 12288 bytes (12.00 KB) 耗時(shí): 2.3456 秒 吞吐量: 426315 ops/sec
結(jié)論:
- 內(nèi)存占用:僅 12KB,遠(yuǎn)低于 Set 結(jié)構(gòu)的 80MB(100 萬(wàn)用戶)。
- 統(tǒng)計(jì)精度:誤差率 0.85%,符合 HyperLogLog 的標(biāo)準(zhǔn)誤差范圍(< 0.81% 為理論值,實(shí)際略有波動(dòng))。
- 寫(xiě)入性能:每秒可處理 42 萬(wàn)次添加操作,完全滿足高并發(fā)場(chǎng)景。
3.2 UV 統(tǒng)計(jì)架構(gòu)設(shè)計(jì)
Key 設(shè)計(jì)規(guī)范:
# 日級(jí) UV 統(tǒng)計(jì)
uv:daily:{YYYY-MM-DD} # 例如: uv:daily:2024-01-15
# 月級(jí) UV 統(tǒng)計(jì)(通過(guò) PFCOUNT 合并日級(jí)數(shù)據(jù))
uv:monthly:{YYYY-MM} # 例如: uv:monthly:2024-01
# 全站歷史 UV(通過(guò) PFMERGE 合并月級(jí)數(shù)據(jù))
uv:lifetime核心實(shí)現(xiàn)代碼:
import redis
from datetime import datetime, timedelta
from typing import List
class UVStatisticsService:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
def record_visit(self, user_id: int, visit_date: datetime = None):
"""
記錄用戶訪問(wèn)
:param user_id: 用戶 ID
:param visit_date: 訪問(wèn)日期(默認(rèn)當(dāng)天)
"""
if visit_date is None:
visit_date = datetime.now()
date_key = visit_date.strftime("%Y-%m-%d")
key = f"uv:daily:{date_key}"
# 添加用戶到當(dāng)日 HyperLogLog
self.redis.pfadd(key, str(user_id))
# 設(shè)置過(guò)期時(shí)間(保留 90 天數(shù)據(jù))
self.redis.expire(key, 90 * 24 * 60 * 60)
def get_daily_uv(self, date: datetime = None) -> int:
"""
獲取指定日期的 UV
:param date: 查詢?nèi)掌冢J(rèn)當(dāng)天)
:return: UV 數(shù)量
"""
if date is None:
date = datetime.now()
date_key = date.strftime("%Y-%m-%d")
key = f"uv:daily:{date_key}"
return self.redis.pfcount(key)
def get_date_range_uv(self, start_date: datetime, end_date: datetime) -> int:
"""
獲取日期范圍內(nèi)的 UV(并集統(tǒng)計(jì))
:param start_date: 開(kāi)始日期
:param end_date: 結(jié)束日期
:return: 去重后的 UV 數(shù)量
"""
keys = []
current_date = start_date
while current_date <= end_date:
key = f"uv:daily:{current_date.strftime('%Y-%m-%d')}"
keys.append(key)
current_date += timedelta(days=1)
if not keys:
return 0
# 使用 PFCOUNT 計(jì)算并集基數(shù)
return self.redis.pfcount(*keys)
def merge_monthly_uv(self, year: int, month: int):
"""
合并月度 UV 統(tǒng)計(jì)
:param year: 年份
:param month: 月份
"""
# 生成該月所有日期的 Key
keys = []
date = datetime(year, month, 1)
while date.month == month:
key = f"uv:daily:{date.strftime('%Y-%m-%d')}"
keys.append(key)
date += timedelta(days=1)
# 合并到月度 Key
monthly_key = f"uv:monthly:{year}-{month:02d}"
self.redis.pfmerge(monthly_key, *keys)
# 設(shè)置過(guò)期時(shí)間(保留 2 年)
self.redis.expire(monthly_key, 2 * 365 * 24 * 60 * 60)FastAPI 接口實(shí)現(xiàn):
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime, timedelta
from pydantic import BaseModel
router = APIRouter()
class UVStatsResponse(BaseModel):
daily_uv: int
weekly_uv: int
monthly_uv: int
@router.get("/stats/uv", response_model=UVStatsResponse)
async def get_uv_statistics(
date: str = None,
uv_service: UVStatisticsService = Depends(lambda: UVStatisticsService(redis_client))
):
"""
獲取 UV 統(tǒng)計(jì)數(shù)據(jù)
:param date: 查詢?nèi)掌冢╕YYY-MM-DD 格式,默認(rèn)當(dāng)天)
:return: 日、周、月 UV 統(tǒng)計(jì)
"""
if date:
try:
query_date = datetime.strptime(date, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format")
else:
query_date = datetime.now()
# 日 UV
daily_uv = uv_service.get_daily_uv(query_date)
# 周 UV(最近 7 天)
week_start = query_date - timedelta(days=6)
weekly_uv = uv_service.get_date_range_uv(week_start, query_date)
# 月 UV(最近 30 天)
month_start = query_date - timedelta(days=29)
monthly_uv = uv_service.get_date_range_uv(month_start, query_date)
return UVStatsResponse(
daily_uv=daily_uv,
weekly_uv=weekly_uv,
monthly_uv=monthly_uv
)
@router.post("/visit/record")
async def record_user_visit(
user_id: int,
uv_service: UVStatisticsService = Depends(lambda: UVStatisticsService(redis_client))
):
"""
記錄用戶訪問(wèn)
:param user_id: 用戶 ID
"""
uv_service.record_visit(user_id)
return {"status": "success", "message": "Visit recorded"}四、 HyperLogLog 與 Bitmap、Set 的對(duì)比選型
| 數(shù)據(jù)結(jié)構(gòu) | 內(nèi)存占用 | 精確度 | 適用場(chǎng)景 | 核心命令 |
|---|---|---|---|---|
| HyperLogLog | < 16KB(固定) | 誤差 < 0.81% | 海量 UV 統(tǒng)計(jì)、去重計(jì)數(shù) | PFADD, PFCOUNT |
| Bitmap | N bits(N=最大值) | 100% 精確 | 連續(xù)整數(shù) ID 簽到、狀態(tài)標(biāo)記 | SETBIT, GETBIT, BITCOUNT |
| Set | N × 8 bytes | 100% 精確 | 中小規(guī)模去重集合、交集/并集運(yùn)算 | SADD, SISMEMBER, SINTER |
選型建議:
- UV 統(tǒng)計(jì)(千萬(wàn)級(jí)以上):HyperLogLog,內(nèi)存占用極低,誤差可接受。
- 用戶簽到(連續(xù)日期):Bitmap,按日期偏移量存儲(chǔ),支持連續(xù)簽到統(tǒng)計(jì)。
- 共同關(guān)注/好友列表(萬(wàn)級(jí)以下):Set,支持交集、并集等集合運(yùn)算,精確度高。
五、 總結(jié)
5.1 核心收益
- 極致內(nèi)存效率:?jiǎn)稳?UV 統(tǒng)計(jì)僅需 12KB 內(nèi)存,全年 365 天累計(jì)僅 4.5MB,相比 Set 結(jié)構(gòu)節(jié)省 99.99% 內(nèi)存。
- 高性能寫(xiě)入:?jiǎn)螜C(jī) Redis 可支撐百萬(wàn)級(jí) QPS 的 PV 記錄,滿足億級(jí)日活產(chǎn)品的統(tǒng)計(jì)需求。
- 靈活聚合能力:通過(guò)
PFCOUNT與PFMERGE實(shí)現(xiàn)跨天、跨維度的并集統(tǒng)計(jì),支持周報(bào)、月報(bào)等復(fù)雜分析場(chǎng)景。
5.2 優(yōu)化建議
- TTL 過(guò)期策略:為日級(jí) UV Key 設(shè)置 90 天 TTL,自動(dòng)清理歷史數(shù)據(jù),防止內(nèi)存無(wú)限增長(zhǎng)。
- 月度歸檔:通過(guò)定時(shí)任務(wù)執(zhí)行
PFMERGE,將日級(jí)數(shù)據(jù)合并為月度 Key,便于長(zhǎng)期趨勢(shì)分析。 - 誤差容忍:業(yè)務(wù)層需明確 HyperLogLog 的誤差特性,避免在財(cái)務(wù)結(jié)算等強(qiáng)一致性場(chǎng)景使用。
- 監(jiān)控告警:監(jiān)控 Redis 內(nèi)存使用率與 HyperLogLog Key 數(shù)量,設(shè)置閾值告警,防止內(nèi)存溢出。
到此這篇關(guān)于Redis HyperLogLog用戶統(tǒng)計(jì)功能實(shí)現(xiàn)代碼的文章就介紹到這了,更多相關(guān)Redis HyperLogLog用戶統(tǒng)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Redis?HyperLogLog數(shù)據(jù)量統(tǒng)計(jì)的實(shí)現(xiàn)實(shí)例
- Redis?HyperLogLog數(shù)據(jù)統(tǒng)計(jì)輕量級(jí)解決方案詳解
- Redis特殊數(shù)據(jù)類型HyperLogLog基數(shù)統(tǒng)計(jì)算法講解
- Redis中?HyperLogLog數(shù)據(jù)類型使用小結(jié)
- Redis中HyperLogLog的使用詳情
- Redis如何使用HyperLogLog的實(shí)現(xiàn)
- Redis高級(jí)數(shù)據(jù)類型Hyperloglog、Bitmap的使用
- Redis中3種特殊的數(shù)據(jù)類型(BitMap、Geo和HyperLogLog)
相關(guān)文章
如何使用redis的setnx實(shí)現(xiàn)分布式鎖
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在時(shí),為 key 設(shè)置指定的值,這篇文章主要介紹了使用redis的setnx實(shí)現(xiàn)分布式鎖,需要的朋友可以參考下2024-06-06
Redis實(shí)現(xiàn)限流器的三種方法(小結(jié))
本文主要介紹了Redis實(shí)現(xiàn)限流器的三種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
SpringMVC集成redis配置的多種實(shí)現(xiàn)方法
這篇文章主要介紹了SpringMVC集成redis配置的多種實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Redis過(guò)期監(jiān)聽(tīng)機(jī)制,訂單超時(shí)自動(dòng)取消方式
這篇文章主要介紹了Redis過(guò)期監(jiān)聽(tīng)機(jī)制,訂單超時(shí)自動(dòng)取消方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
Redis并發(fā)訪問(wèn)問(wèn)題詳細(xì)講解
本文主要介紹了Redis如何應(yīng)對(duì)并發(fā)訪問(wèn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-12-12

