若是如今要開發一個功能:html
統計APP或網頁的一個頁面,天天有多少用戶點擊進入的次數。同一個用戶的反覆點擊進入記爲 1 次,也就是統計 UV 數據。redis
讓你來開發這個統計模塊,你會如何實現?算法
若是統計 PV 數據,只要給網頁一個獨立的 Redis 計數器就能夠了,這個計數器的 key 的格式爲 puv:{pid}:{yyyyMMdd}
。每來一個請求就 incrby 一次,就能夠統計出全部的 PV 數據。數據結構
可是 UV 不同,它要去重,同一個用戶一天以內的屢次訪問請求只能計數一次。這就要求每個網頁請求都須要帶上用戶的 ID,不管是登錄用戶仍是未登錄用戶都須要一個惟一 ID 來標識。ide
你可能會立刻想到,用 Hash
數據類型就能知足去重。這確實是一種解決方法,可是當這個頁面的日活達到百萬或千萬以上級別的話,Hash
的內存開銷就會很是大。post
咱們來估算一下采用 Hash
的內存空間是多大。假設 key
是 int
類型,對應的是用戶ID,value
是 bool
類型,表示已訪問,當有百萬級不一樣用戶訪問時,內存空間爲:100萬 * (32+8)bit = 40MB
。優化
那有更好的方法嗎?有的,下面來介紹基於 HyperLogLog
的解決方案。首先咱們先來了解一下 HyperLogLog
。ui
HyperLogLog
的做用是提供不精確的去重計數方案。雖然不精確,但也不是很是不精確,標準偏差是 0.81%
,這樣的精確度已經能夠知足上面的 UV 統計需求了。url
它的優勢是使用極少的內存就能統計大量的數據,Redis 實現的 HyperLogLog,只須要 12K
內存就能統計 $2^64$
個數據。遠比 Hash
的內存開銷要少。spa
HyperLogLog(HLL)
是一種用於基數計數的機率算法,是基於 LogLog(LLC)
算法的優化和改進,在一樣空間複雜度下,可以比 LLC 的基數估計偏差更小。
HyperLogLog
算法的通俗說明:假設咱們爲一個數據集合生成一個8位的哈希串,那麼咱們獲得00000111的機率是很低的,也就是說,咱們生成大量連續的0的機率是很低的。生成連續5個0的機率是1/32,那麼咱們獲得這個串時,能夠估算,這個數據集的基數是32。
再深刻的那就是數學公式,可參考本文最後的參考連接前往研究。
命令 | 說明 | 可用版本 | 時間複雜度 |
PFADD | 添加 | >= 2.8.9 | O(1) |
PFCOUNT | 得到基數值 | >= 2.8.9 | O(1) |
PFMERGE | 合併多個key | >= 2.8.9 | O(N) |
using StackExchange.Redis;using System;public class PageUVDemo { private static IDatabase db; static void Main(string[] args) { ConnectionMultiplexer connection = ConnectionMultiplexer.Connect("192.168.0.104:7001,password=123456"); db = connection.GetDatabase(); Console.WriteLine("hll:"); HLLVisit(1000, 1000); HLLVisit(10000, 10000); HLLVisit(100000, 100000); Console.WriteLine("hash:"); HashVisit(1000, 1000); HashVisit(10000, 10000); HashVisit(100000, 100000); connection.Close(); } static void HLLVisit(int times, int pid) { string key = $"puv:hll:{pid}"; DateTime start = DateTime.Now; for (int i = 0; i < times; i++) { db.HyperLogLogAdd(key, i); } long total = db.HyperLogLogLength(key); DateTime end = DateTime.Now; Console.WriteLine("插入{0}次:", times); Console.WriteLine(" total:{0}", total); Console.WriteLine(" duration:{0:F2}s", (end - start).TotalSeconds); Console.WriteLine(); } static void HashVisit(int times, int pid) { string key = $"puv:hash:{pid}"; DateTime start = DateTime.Now; for (int i = 0; i < times; i++) { db.HashSet(key, i, true); } long total = db.HashLength(key); DateTime end = DateTime.Now; Console.WriteLine("插入{0}次:", times); Console.WriteLine(" total:{0}", total); Console.WriteLine(" duration:{0:F2}s", (end - start).TotalSeconds); Console.WriteLine(); } }
運行結果
結果對比
數據經過 redis-rdb-tools
導出,更多請查看。
數據類型 | 插入次數 | 內存開銷 | 時間開銷 | 偏差率 |
hash | 1000 | 35KB | 3.45s | 0% |
10000 | 426KB | 34.65s | 0% | |
100000 | 3880KB | 342.36s | 0% | |
hll | 1000 | 2KB | 3.57s | 0.1% |
10000 | 14KB | 33.25s | 0.13% | |
100000 | 14KB | 307.80s | 0.44% |
從上面的結果能夠看出,10萬次級別下,HyperLogLog 的偏差率很低,0.44%,但內存開銷是 Hash 的0.3%,隨着數量級的提高,內存開銷差距也越大。
不追求百分百的準確度時,使用 HyperLogLog 數據結構能減小內存開銷。