楔子
在我們實際開發的過程中,可能會遇到這樣一個問題,當我們需要統計一個大型網站的獨立訪問次數時,該用什么的類型來統計?
如果我們使用 Redis 中的集合來統計,當它每天有數千萬級別的訪問時,將會是一個巨大的問題。因為這些訪問量不能被清空,我們運營人員可能會隨時查看這些信息,那么隨着時間的推移,這些統計數據所占用的空間會越來越大,逐漸超出我們能承載最大空間。
例如,我們用 IP 來作為獨立訪問的判斷依據,那么我們就要把每個獨立 IP 進行存儲,以 IP4 來計算,IP4 最多需要 15 個字節來存儲信息,例如:110.110.110.110。當有一千萬個獨立 IP 時,所占用的空間就是 15 bit * 10000000 約定於 143MB,但這只是一個頁面的統計信息,假如我們有 1 萬個這樣的頁面,那我們就需要 1T 以上的空間來存儲這些數據。而且隨着 IP6 的普及,這個存儲數字會越來越大,那我們就不能用集合的方式來存儲了,這個時候我們需要開發新的數據類型來做這件事了,而這個新的數據類型就是我們今天要介紹的HyperLogLog。
HyperLogLog介紹與使用
HyperLogLog(下文簡稱為 HLL)是 Redis 2.8.9 版本添加的數據結構,它用於高性能的基數(去重)統計功能,它的缺點就是存在極低的誤差率
HLL 具有以下幾個特點:
能夠使用極少的內存來統計巨量的數據,它只需要 12K 空間就能統計 2^64 的數據;
統計存在一定的誤差,誤差率整體較低,標准誤差為 0.81%;
誤差可以被設置輔助計算因子進行降低。
HLL 的命令只有 3 個,但都非常的實用,下面分別來看。
添加元素
pfadd key element1 element2······,可以同時添加多個。
127.0.0.1:6379> pfadd hll1 mea
(integer) 1
127.0.0.1:6379> pfadd hll1 kano nana
(integer) 1
127.0.0.1:6379> pfadd hll1 mea
(integer) 0
127.0.0.1:6379>
統計不重復的元素個數
pfcount key1 key2····,可以同時統計多個HHL結構。
127.0.0.1:6379> pfcount hll1
(integer) 3 # 不重復元素個數有3個
127.0.0.1:6379>
將多個HLL結構中元素移動到新的HLL結構中
pfmerge key key1 key2····,將key1、key2····移動到key中。
127.0.0.1:6379> pfadd hll1 mea kano nana
(integer) 1
127.0.0.1:6379> pfadd hll2 mea kano yume
(integer) 1
127.0.0.1:6379> pfmerge hll hll1 hll2
OK
127.0.0.1:6379> pfcount hll
(integer) 4
127.0.0.1:6379>
當我們需要合並兩個或多個同類頁面的訪問數據時,我們可以使用 pfmerge 來操作。
Python實現HLL相關操作
import redis
client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
# 1. pfadd key1 key2···
client.pfadd("HLL1", "a", "b", "c")
client.pfadd("HLL2", "b", "c", "d")
# 2. pfcount key1 key2···
print(client.pfcount("HLL1", "HLL2")) # 4
# 3. pfmerge key key1 key2···
client.pfmerge("HLL", "HLL1", "HLL2")
print(client.pfcount("HLL")) # 4
HyperLogLog算法原理
HyperLogLog 算法來源於論文HyperLogLog the analysis of a near-optimal cardinality estimation algorithm
,想要了解 HLL 的原理,先要從伯努利試驗說起,伯努利實驗指的是在同樣的條件下重復地、相互獨立地進行的一種隨機試驗,其特點是該隨機試驗只有兩種可能結果:發生或者不發生。我們假設該項試驗獨立重復地進行了n次,那么就稱這一系列重復獨立的隨機試驗為n重伯努利試驗,或稱為伯努利概型。比如最經典、也是最好理解的拋硬幣,每一次拋出的硬幣都是各自獨立的,當前拋出的硬幣不受上一次的影響。
注意:單個伯努利試驗是沒有多大意義的,然而,當我們反復進行伯努利試驗,去觀察這些試驗有多少是成功的,多少是失敗的,事情就變得有意義了,這些累計記錄包含了很多潛在的非常有用的信息。
並且根據大數定理我們知道,如果一個事件發生的概率是恆定的,那么隨着試驗次數的增加,那么該事件的頻率越接近概率。還拿拋硬幣舉例,假設你拋硬幣拋了四次,全是正面(這種情況是可能出現的)
,難道我們就說拋出一枚硬幣,正面朝上的概率是百分之百嗎?顯然不能,而大數定理會告訴我們,只要你拋出硬幣的次數足夠多,你會發現正面出現的次數除以拋出的總次數會無限接近二分之一。
之所以說這些,是因為Redis采用的算法不是按照類似我們上面說的方式,因為大數定理對於數據量小的時候,會有很大的誤差。而為了解決這個問題,HLL 引入了分桶算法和調和平均數來使這個算法更接近真實情況。
分桶算法是指把原來的數據平均分為 m 份,在每段中求平均數在乘以 m,以此來消減因偶然性帶來的誤差,提高預估的准確性,簡單來說就是把一份數據分為多份,把一輪計算,分為多輪計算。
而調和平均數指的是使用平均數的優化算法,而非直接使用平均數。
例如小明的月工資是 1000 元,而小王的月工資是 100000 元,如果直接取平均數,那小明的平均工資就變成了 (1000+100000)/2=50500 元,這顯然是不准確的,而使用調和平均數算法計算的結果是 2/(1/1000+1/100000)≈1998 元,顯然此算法更符合實際平均數。
所以綜合以上情況,在 Redis 中使用 HLL 插入數據,相當於把存儲的值經過 hash 之后,再將 hash 值轉換為二進制,存入到不同的桶中,這樣就可以用很小的空間存儲很多的數據,統計時再去相應的位置進行對比很快就能得出結論,這就是 HLL 算法的基本原理,想要更深入的了解算法及其推理過程,可以看去原版的論文,鏈接地址:http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf
。
小結
當需要做大量數據統計時,普通的集合類型已經不能滿足我們的需求了,這個時候我們可以借助 Redis 2.8.9 中提供的 HyperLogLog 來統計,它的優點是只需要使用 12k 的空間就能統計 2^64 的數據,但它的缺點是存在 0.81% 的誤差,HyperLogLog 提供了三個操作方法:pfadd 添加元素、pfcount 統計元素和 pfmerge 合並元素。