在我們做站點流量統計的時候一般會統計頁面UV(獨立訪客:unique visitor)和PV(即頁面瀏覽量:page view),那麼我們最常見的處理方式就是用戶點擊一次就插入一條數據到數據庫,統計的時候通過查詢表來達到統計流量的效果。
那麼我們如果是通過redis來處理,我們可以使用string類型然後自增計數即可達到統計PV,統計UV可以使用set,每個用戶id是唯一的可以放到這個集合裏,統計的時候只需要執行scard獲取集合大小即可。
這兩種方式都是可以實現站點的流量統計,但是如果說當站點流量非常大每天幾千萬次的訪問量,那麼數據庫可能就要分表分庫了,redis添加足夠多的數據後內存消耗也是非常驚人的,總的來說這樣做是不划算的,那麼我們的redis有一種專門處理這樣數據的算法,HyperLogLog。
HyperLogLog基本使用方式
HyperLogLog提供了兩個指令pfadd和pfcount,根據字面意思很好理解,一個是增加計數,一個是獲取計數。pfadd和set集合的sadd的用法是一樣的,pfcount和scard的用法是一樣的,直接獲取計數值。
HyperLogLog還提供一個合併的指令pfmerge,用於將兩個pf累加形成一個新的pf,如果說我們需要將每個頁面的流量合併,那麼我們這個指令大有用處。
> pfadd user mango
(integer) 1
> pfadd user zhangsan
(integer) 1
> pfadd user lisi
(integer) 1
> pfadd user mango #重複則不計數
(integer) 0
> pfcount user
(integer) 3
> pfadd paper mango
(integer) 1
> pfadd paper zhangsan
(integer) 1
> pfmerge pv user paper #合併
OK
> pfcount pv
(integer) 3
注意:
-
HyperLogLog的計數統計是有一定的誤差的,誤差最大在1%以下。
-
HyperLogLog數據結構需要佔據12KB的存儲空間,所以我們在使用的時候得注意,如果數據量非常小我們可以選擇其他方式,但是如果是數據量非常大,那麼我們這個數據結構就非常有價值了。但是我們也不要太過於擔心它的消耗,一開始初始的時候是沒有這麼大的,在計數比較小時他是採用稀疏矩陣來進行存儲,空間佔用率還是很小的,只有到達一定閾值後纔會轉變成稠密矩陣空間纔會到達12KB。
命令pf前綴的由來
我們使用這個HyperLogLog命令的時候有點奇怪的感覺,因爲我們使用hash的時候都是h開頭hadd,set都是sadd,zset都是z開頭,那麼我們的HyperLogLog爲啥是個pf呢?HyperLogLog這個數據結構的發明人Philippe Flajolet, pf 是他的名字的首字母縮寫。
HyperLogLog實現原理
HyperLogLog實際上不會存儲每個元素的值,它使用的是概率算法,通過存儲元素的hash值的第一個1的位置,來計算元素數量。
我們第一次產生的隨機值獲取到它的二進制,從右往左計數連續0的個數,每次獲取這個數出現1和0的概率都是1/2,k在任意回合出現的概率即爲(1/2)^k,因此可以推測n=2^k,但是一般來講n的值會在2^k和2^k之間徘徊,爲了要讓這個算法更加準確,於是引入了桶的概念,計算m個桶的加權平均值,這樣就能得到比較準確的答案了(實際上還要進行其他修正)。最終的公式如圖
其中m是桶的數量,const是修正常數,它的取值會根據m而變化。p=log2m
switch (p) {
case 4:
constant = 0.673 * m * m;
case 5:
constant = 0.697 * m * m;
case 6:
constant = 0.709 * m * m;
default:
constant = (0.7213 / (1 + 1.079 / m)) * m * m;
}
有興趣看源碼的同學可以去github上查看redis HyperLogLog的代碼實現,考慮到篇幅太多,讀起來緊張我們這裏就不貼源代碼了
HyperLogLog相關的命令有三個:
pfadd hll ele:將ele添加進hll的基數計算中。流程:
-
先對ele求hash(使用的是一種叫做MurMurHash的算法)
-
將hash的低14位(因爲總共有2的14次方個桶)作爲桶的編號,選桶,記桶中當前的值爲count
-
從的hash的第15位開始數0,假設從第15位開始有n個連續的0(即前導0)
-
如果n大於count,則把選中的桶的值置爲n,否則不變
pfcount hll:計算hll的基數。就是使用上面給出的DV公式根據桶中的數值,計算基數
pfmerge hll3 hll1 hll2:將hll1和hll2合併成hll3。合併方法是將所有的HyperLogLog對象合併到一個名爲max的對象中,max採用的 是密集存儲結構,如果被合併的對象也是密集存儲結構,則循環比較每一個計數值,將大的那個存入max。
Redis的所有HyperLogLog結構都是固定的16384個桶(2的14次方),並且有兩種存儲格式:
-
稀疏格式:HyperLogLog算法在剛開始的時候,大多數桶其實都是0,稀疏格式通過存儲連續的0的數目,而不是每個0存一遍,大大減小了HyperLogLog剛開始時需要佔用的內存
-
緊湊格式:用6個bit表示一個桶,需要佔用12KB內存
pf 的內存佔用爲什麼是 12k?
我們在上面的算法中使用了1024個桶進行獨立計數,不過在Redis的HyperLogLog實現中用到的是16384個桶,也就是2^14,每個桶的maxbits需要6個bits來存儲,最大可以表示maxbits=63,於是總共佔用內存就是2^14 * 6/8 = 12k 字節。
一名正在搶救的coder
筆名:mangolove
CSDN地址:https://blog.csdn.net/mango_love
GitHub地址:https://github.com/mangoloveYu