關注公衆號:xy的技術圈java
在應用系統的開發中,咱們經常會有相似這樣的需求:統計某個網站的UV、用戶搜索網站關鍵詞的數量等等。咱們可使用基數計數來作這個功能。基數計數一般用來統計一個集合中不重複的元素個數。git
在應用程序的數據分析、網絡監控及數據庫優化等等地方都須要基數計數。github
要實現基數計數,最簡單的方式就是使用一個set,把出現的元素add進去,而後計算set的size。但若是數據量較大,使用set就會浪費大量的空間。redis
前面的文章介紹了bitmaps,使用bitmaps也能夠作基數計數。但若是數據量較大,使用bitmaps一樣會有這個問題。若是要統計一億個數據的基數值,大約須要12M內存。若是使用32位的int類型來表明每一個數據,就須要32 * 12約爲381M。算法
可見,使用bitmaps仍是不適用大數據量下的基數計數場景。spring
連bitmaps都不合適,那還有更好的方法來實現大數據的基數計數嗎?數據庫
固然有,數學是神奇的。咱們使用一些機率論的數學原理,在必定偏差條件下,能夠高效地估計出基數的近似值。網絡
具體是什麼算法呢?數據結構
LogLog Counting(LLC)被髮明出來解決這個問題,空間複雜度只有log(log(N))。但LLC偏差較大,HyperLogLog Counting(HLL)在LLC的基礎上進行了改進,在一樣空間複雜度狀況下,可以比LLC的基數估計偏差更小。函數
HLL有多強大?redis中實現的HyperLogLog,只須要12K內存,在標準偏差0.81%的前提下,可以統計2^64
個數據。
可是,由於HyperLogLog只會根據輸入元素來計算基數,而不會儲存輸入元素自己,因此HyperLogLog不能像集合那樣,返回輸入的各個元素。
如下內容請謹慎食用
HyperLogLog算法來源於論文《HyperLogLog the analysis of a near-optimal cardinality estimation algorithm》
舉一個例子,假設你拋不少次硬幣,若是拋到正面,就繼續拋;若是拋到反面,就記錄下在這以前連續拋到了多少次正面k,而後開始下一輪。
若是你告訴我,你最多的時候,連續拋了2次正面後就拋到反面了。那我認爲你可能並無拋多少輪,多是3輪或者4輪就會發生這樣的狀況。
但若是你告訴我,你最多的時候,連續拋了10次正面後就拋到反面了,那我認爲你可能拋的輪次比較多,由於連續拋到10次正面的機率是很是小的。那若是要根據這個已知信息估計你總共拋了多少輪硬幣呢?這就是HLL的原理。
HLL背後是一個著名的數學上的機率論原理:伯努利分佈。一樣是上面那個拋硬幣例子,出現正反面的機率都是1/2,一直拋硬幣直到出現正面,記錄下投擲次數k,將這種拋硬幣屢次直到出現正面的過程記爲一次伯努利過程,對於n次伯努利過程,咱們會獲得n個出現正面的投擲次數值k1, k2, k3……kn,其中最大值記爲k_max,能夠用n次實驗中最大的拋擲次數k_max來預估實驗組數量n: 有如下公式
n = 2 ^ k_max
具體推導過程有點麻煩,感興趣的朋友能夠下來本身去研究一下。
回到基數統計的問題,咱們須要統計一組數據中不重複元素的個數,集合中每一個元素的通過hash函數後能夠表示成0和1構成的二進制數串,一個二進制串能夠類比爲一次拋硬幣實驗,1是拋到正面,0是反面。
二進制串中從低位開始第一個1出現的位置能夠理解爲拋硬幣試驗中第一次出現正面的拋擲次數k,那麼基於上面的結論,咱們能夠經過屢次拋硬幣實驗的最大拋到正面的次數來預估總共進行了多少次實驗,一樣能夠能夠經過第一個1出現位置的最大值k_max來預估總共有多少個不一樣的數字(總體基數)。
那根據上面的公式,就能夠計算出基數n了。
但這樣偏差仍是有點大,並且只能是2的指數,顯然並不合理。
既然偏差大,那就想辦法下降偏差。LLC的作法是把全部的數分到不一樣的桶中,獲得每一個桶的估計值n1, n2, n3……而後計算它們的幾何平均數。
但這樣偏差仍是有點大,特別是在數據量不大的時候,某個n可能會比較大,會大幅拉昇總體的評論數。好比個人工資是1000元,老闆的工資是10,000元,那咱們工資的幾何平均數就是(1000 + 10,000) / 2 = 50500元。看來我又「被平均」了,我以爲這樣並不公平,不能顯示咱們公司真實的薪資情況。因而咱們使用調和平均數的方式來計算:
2 / (1/1000 + 1/10,000) = 1980.2
嗯,這樣看起來是比較接近真實水平的。「調和平均數」的結果會傾向於集合中比較小的數。而HLL正是在LLC的基礎上,使用了調和平均數。
Redis在2.8.9版本添加了HyperLogLog結構。
Redis首先對元素進行hash,生成64bit的數。其中14bit用來分桶,其他50位用來計算n值。每一個桶所佔用的元素是相對平均的。
它的實現中,設有16384個桶,即:2^14 = 16384,每一個桶有6位,桶中存放的是這個桶的k_max的值,每一個桶能夠表達的最大數字是63,二進制爲:111 111
。全部桶加起來佔用內存爲=16834 * 6 / 8 / 1024 = 12K。
第0組 第1組 .... 第16833組 [000 000] [000 000] [000 000] [000 000] .... [000 000]
爲何是14位,爲何是16384個桶?這是根據「相對標準偏差」(relative standard deviation,簡稱RSD)公式計算得來的。RSD的值與分桶數m存在以下的計算關係:
由於咱們能夠將每一個元素的hash值的二進制表示的前幾位用來指示數據屬於哪一個桶,因此桶的數量一定是2的指數。前文提到了,Redis的標準偏差是0.81%,因此獲得m爲2^14
。
Redis在元素不多的時候使用的是稀疏矩陣,因此並不會用到12k。
stream-lib是一個開源的Java流式計算庫,裏面有不少大數據估值算法的實現,裏面就有HyperLogLog算法,HyperLogLog實現類的代碼地址以下,有興趣的朋友能夠去研究一下: github.com/addthis/str…
Redis中HLL主要有三個命令:PFADD
、PFCOUNT
、PFMERGE
。分別是添加、統計、合併兩個key。
PF是什麼意思?
它是HyperLogLog這個數據結構的發明人Philippe Flajolet的首字母縮寫
認真寫文章,用心作分享。
我的網站:yasinshaw.com
公衆號:xy的技術圈