做者:林冠宏 / 指尖下的幽靈html
掘金:juejin.im/user/587f0d…java
博客:www.cnblogs.com/linguanh/python
GitHub : github.com/af913337456…git
騰訊雲專欄: cloud.tencent.com/developer/u…github
蟲洞區塊鏈專欄:www.chongdongshequ.com/article/153…golang
PS:本人技術書籍:《區塊鏈以太坊DApp開發實戰》已經出版並能夠網購了redis
若是要實現這麼一個功能:算法
統計 APP或網頁 的一個頁面,天天有多少用戶點擊進入的次數。同一個用戶的反覆點擊進入記爲 1 次。編程
聰明的你可能會立刻想到,用 HashMap
這種數據結構就能夠了,也知足了去重。的確,這是一種解決方法,除此以外還有其它的解決方案。數組
問題雖不難,但當參與問題中的變量達到必定數量級的時候,再簡單的問題都會變成一個難題。假設 APP 中日活用戶達到百萬
或千萬以上級別
的話,咱們採用 HashMap
的作法,就會致使程序中佔用大量的內存。
咱們下面嘗試估算下 HashMap
的在應對上述問題時候的內存佔用。假設定義HashMap
中 Key
爲 string
類型,value
爲 bool
。key
對應用戶的Id
,value
是是否點擊進入
。明顯地,當百萬不一樣用戶訪問的時候。此HashMap
的內存佔用空間爲:100萬 * (string + bool)
。
能夠說,在上述問題目前現有的解決方案中,HashMap
是內存佔用量最多的一種。若是統計量很少,那麼可使用這種方法解決問題,實現起來也簡單。
除此以外還有B+ 樹
,Bitmap 位圖
,以及該文章主要介紹的 HyperLogLog
算法解決方案。
在必定條件容許下,若是容許統計在巨量數據面前的偏差率在可接受的範圍內,1000萬瀏覽量容許最終統計出少了一兩萬這樣子,那麼就能夠採用HyperLogLog
算法來解決上面的計數相似問題。
HyperLogLog
,下面簡稱爲HLL
,它是 LogLog
算法的升級版,做用是可以提供不精確的去重計數。存在如下的特色:
Redis
中實現的 HyperLogLog
,只須要12K
內存就能統計2^64
個數據。輔助計算因子
進行下降。稍微對編程中的基礎數據類型內存佔用有了解的同窗,應該會對其只須要12K
內存就能統計2^64
個數據而感到驚訝。爲何這樣說呢,下面咱們舉下例子:
取 Java
語言來講,通常long
佔用8字節,而一字節有8位,即:1 byte = 8 bit,即long
數據類型最大能夠表示的數是:2^63-1
。對應上面的2^64
個數,假設此時有2^63-1
這麼多個數,從 0 ~ 2^63-1
,按照long
以及1k = 1024字節
的規則來計算內存總數,就是:((2^63-1) * 8/1024)K
,這是很龐大的一個數,存儲空間遠遠超過12K
。而 HyperLogLog
卻能夠用 12K
就能統計完。
在認識爲何HyperLogLog
可以使用極少的內存來統計巨量的數據以前,要先認識下伯努利試驗
。
伯努利試驗
是數學機率論
中的一部份內容,它的典故來源於拋硬幣
。
硬幣擁有正反兩面,一次的上拋至落下,最終出現正反面的機率都是50%。假設一直拋硬幣,直到它出現正面爲止,咱們記錄爲一次完整的試驗,間中可能拋了一次就出現了正面,也可能拋了4次纔出現正面。不管拋了多少次,只要出現了正面,就記錄爲一次試驗。這個試驗就是伯努利試驗
。
那麼對於屢次的伯努利試驗
,假設這個屢次爲n
次。就意味着出現了n
次的正面。假設每次伯努利試驗
所經歷了的拋擲次數爲k
。第一次伯努利試驗
,次數設爲k1
,以此類推,第n
次對應的是kn
。
其中,對於這n
次伯努利試驗
中,必然會有一個最大的拋擲次數k
,例如拋了12次纔出現正面,那麼稱這個爲k_max
,表明拋了最多的次數。
伯努利試驗
容易得出有如下結論:
最終結合極大似然估算的方法,發如今n
和k_max
中存在估算關聯:n = 2^(k_max)
。這種經過局部信息預估總體數據流特性的方法彷佛有些超出咱們的基本認知,須要用機率和統計的方法才能推導和驗證這種關聯關係。
例以下面的樣子:
第一次試驗: 拋了3次纔出現正面,此時 k=3,n=1
第二次試驗: 拋了2次纔出現正面,此時 k=2,n=2
第三次試驗: 拋了6次纔出現正面,此時 k=6,n=3
第n 次試驗:拋了12次纔出現正面,此時咱們估算, n = 2^12
複製代碼
假設上面例子中實驗組數共3組,那麼 k_max = 6,最終 n=3,咱們放進估算公式中去,明顯: 3 ≠ 2^6 。也便是說,當試驗次數很小的時候,這種估算方法的偏差是很大的。
在上面的3組例子中,咱們稱爲一輪的估算。若是隻是進行一輪的話,當 n 足夠大的時候,估算的偏差率會相對減小,但仍然不夠小。
那麼是否能夠進行多輪呢?例如進行 100 輪或者更多輪次的試驗,而後再取每輪的 k_max,再取平均數,即: k_mx/100
。最終再估算出 n。下面是LogLog
的估算公式:
上面公式的DVLL
對應的就是n
,constant
是修正因子,它的具體值是不定的,能夠根據實際狀況而分支設置。m
表明的是試驗的輪數。頭上有一橫的R
就是平均數:(k_max_1 + ... + k_max_m)/m
。
這種經過增長試驗輪次,再取k_max
平均數的算法優化就是LogLog
的作法。而 HyperLogLog
和LogLog
的區別就是,它採用的不是平均數
,而是調和平均數
。調和平均數
比平均數
的好處就是不容易受到大的數值的影響。下面舉個例子:
求平均工資:
A的是1000/月,B的30000/月。採用平均數的方式就是: (1000 + 30000) / 2 = 15500
採用調和平均數的方式就是: 2/(1/1000 + 1/30000) ≈ 1935.484
明顯地,調和平均數
比平均數
的效果是要更好的。下面是調和平均數
的計算方式,∑
是累加符號。
上面的內容咱們已經知道,在拋硬幣的例子中,能夠經過一次伯努利試驗中出現的k_max
來估算n
。
那麼這種估算方法如何和下面問題有所關聯呢?
統計 APP或網頁 的一個頁面,天天有多少用戶點擊進入的次數。同一個用戶的反覆點擊進入記爲 1 次
HyperLogLog
是這樣作的。對於輸入的數據,進行下面幾個步驟:
經過hash
函數,將數據轉爲比特串
,例如輸入5,便轉爲:101。爲何要這樣轉化呢?
是由於要和拋硬幣對應上,比特串
中,0 表明了反面,1 表明了正面,若是一個數據最終被轉化了 10010000
,那麼從右往左,從低位往高位看,咱們能夠認爲,首次出現 1 的時候,就是正面。
那麼基於上面的估算結論,咱們能夠經過屢次拋硬幣實驗的最大拋到正面的次數來預估總共進行了多少次實驗,一樣也就能夠根據存入數據中,轉化後的出現了 1 的最大的位置 k_max 來估算存入了多少數據。
分桶就是分多少輪。抽象到計算機存儲中去,就是存儲的是一個以單位是比特(bit),長度爲 L 的大數組 S ,將 S 平均分爲 m 組,注意這個 m 組,就是對應多少輪,而後每組所佔有的比特個數是平均的,設爲 P。容易得出下面的關係:
在 Redis
中,HyperLogLog
設置爲:m=16834,p=6,L=16834 * 6。佔用內存爲=16834 * 6 / 8 / 1024 = 12K
形象化爲:
第0組 第1組 .... 第16833組
[000 000] [000 000] [000 000] [000 000] .... [000 000]
複製代碼
如今回到咱們的原始APP頁面統計用戶的問題中去。
在這個統計問題中,不一樣的用戶 id 標識了一個用戶,那麼咱們能夠把用戶的 id 做爲被hash
的輸入。即:
hash(id) = 比特串
不一樣的用戶 id,必然擁有不一樣的比特串
。每個比特串
,也必然會至少出現一次 1 的位置。咱們類比每個比特串
爲一次伯努利試驗
。
如今要分輪
,也就是分桶
。因此咱們能夠設定,每一個比特串
的前多少位轉爲10進制後,其值就對應於所在桶的標號。假設比特串
的低兩位用來計算桶下標誌,此時有一個用戶的id的比特串
是:1001011000011。它的所在桶下標爲:11(2) = 1*2^1 + 1*2^0 = 3
,處於第3個桶,即第3輪中。
上面例子中,計算出桶號後,剩下的比特串
是:10010110000,從低位到高位看,第一次出現 1 的位置是 5 。也就是說,此時第3個桶,第3輪的試驗中,k_max = 5
。5 對應的二進制是:101,又由於每一個桶有 p 個比特位。當 p>=3 時,即可以將 101 存進去。
模仿上面的流程,多個不一樣的用戶 id,就被分散到不一樣的桶中去了,且每一個桶有其 k_max。而後當要統計出 mian
頁面有多少用戶點擊量的時候,就是一次估算。最終結合全部桶中的 k_max,代入估算公式,便能得出估算值。
下面是 HyperLogLog
的結合了調和平均數的估算公式,變量釋意和LogLog
的同樣:
首先,在 Redis 中,HyperLogLog 是它的一種高級數據結構。提供有包含但不限於下面兩條命令:
回想一下,原始APP頁面統計用戶的問題。若是 key 對應頁面名稱,value 對應用戶id。那麼問題就剛恰好對應上了。
前面咱們已經認識到,它的實現中,設有 16384 個桶,即:2^14 = 16384,每一個桶有 6 位,每一個桶能夠表達的最大數字是:2^5+2^4+...+1 = 63 ,二進制爲: 111 111
。
對於命令:pfadd key value
在存入時,value 會被 hash 成 64 位,即 64 bit 的比特字符串,前 14 位用來選擇這個 value 的比特串中從右往左
第一個 1 出現的下標位置數值要存到那個桶中去,即前 14 位用來分桶。設第一個1出現位置的數值爲 index 。當 index=5 時,就是: ....10000 [01 0000 0000 0000]
之因此選 14位
來表達桶編號是由於,分了 16384 個桶,而 2^14 = 16384,恰好地,最大的時候能夠把桶利用完,不形成浪費。假設一個字符串的前 14 位是:00 0000 0000 0010 (從右往左看) ,其十進制值爲 2。那麼 index 將會被轉化後放到編號爲 2 的桶。
index 的轉化規則:
首先由於完整的 value 比特字符串是 64 位形式,減去 14 後,剩下 50 位,那麼極端狀況,出現 1 的位置,是在第 50 位,即位置是 50。此時 index = 50。此時先將 index 轉爲 2 進制,它是:110010 。
由於16384 個桶中,每一個桶是 6 bit 組成的。恰好 110010 就被設置到了第 2 號桶中去了。請注意,50 已是最壞的狀況,且它都被容納進去了。那麼其餘的不用想也確定能被容納進去。
由於 fpadd 的 key 能夠設置多個 value。例以下面的例子:
pfadd lgh golang
pfadd lgh python
pfadd lgh java
複製代碼
根據上面的作法,不一樣的 value,會被設置到不一樣桶中去,若是出現了在同一個桶的,即前 14 位值是同樣的,可是後面出現 1 的位置不同。那麼比較原來的 index 是否比新 index 大。是,則替換。否,則不變。
最終地,一個 key 所對應的 16384 個桶都設置了不少的 value 了,每一個桶有一個k_max
。此時調用 pfcount 時,按照前面介紹的估算方式,即可以計算出 key 的設置了多少次 value,也就是統計值。
value 被轉爲 64 位的比特串,最終被按照上面的作法記錄到每一個桶中去。64 位轉爲十進制就是:2^64,HyperLogLog
僅用了:16384 * 6 /8 / 1024 K
存儲空間就能統計多達 2^64 個數。
在估算的計算公式中,constant
變量不是一個定值,它會根據實際狀況而被分支設置,例以下面的樣子。
假設:m爲分桶數,p是m的以2爲底的對數。
// m 爲桶數
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;
}
複製代碼
由簡單的拋硬幣試驗能夠引導出如此的震撼的算法,數學之強大。
感謝下面兩遍博文的指引:
本文全部圖片來源於:
本文內容參考於:
www.rainybowe.com/blog/2017/0…
手動直觀觀察 LogLog
和 HyperLogLog
變化的網站: