Redis中的HyperLogLog淺析

Redis 在 2.8.9 版本添加了 HyperLogLog 結(jié)構(gòu)。
Redis HyperLogLog 是用來做基數(shù)統(tǒng)計的算法。

什么是基數(shù)血公?
比如數(shù)據(jù)集 {1, 3, 5, 7, 5, 7, 8}忱叭, 那么這個數(shù)據(jù)集的基數(shù)集為 {1, 3, 5 ,7, 8}, 基數(shù)(不重復(fù)元素)為5。 基數(shù)估計就是在誤差可接受的范圍內(nèi)塌计,快速計算基數(shù)。

基數(shù)計數(shù)(cardinality counting)通常用來統(tǒng)計一個集合中不重復(fù)的元素個數(shù)侯谁,例如統(tǒng)計某個網(wǎng)站的UV锌仅,或者用戶搜索網(wǎng)站的關(guān)鍵詞數(shù)量。如果統(tǒng)計 PV 那非常好辦墙贱,給每個網(wǎng)頁一個獨(dú)立的 Redis 計數(shù)器就可以了热芹,這個計數(shù)器的 key 后綴加上當(dāng)天的日期。這樣來一個請求惨撇,INCRBY 一次伊脓,最終就可以統(tǒng)計出所有的 PV 數(shù)據(jù)。

但是 UV 不一樣魁衙,它要去重报腔,同一個用戶一天之內(nèi)的多次訪問請求只能計數(shù)一次。這就要求每一個網(wǎng)頁請求都需要帶上用戶的 ID剖淀,無論是登陸用戶還是未登陸用戶都需要一個唯一 ID 來標(biāo)識纯蛾。如果采用元素越多耗費(fèi)內(nèi)存就越多的集合(HashSet、B+ Tree纵隔、BitMap)來存儲已登錄的用戶ID翻诉,那么在輸入元素的數(shù)量或者體積非常大時查重時間(O(log n))與內(nèi)存開銷(O(n))都是十分巨大的。相比之下HyperLogLog 計算基數(shù)所需的空間總是固定的捌刮、并且是很小的碰煌。在 Redis 里面,每個 HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存绅作,就可以計算接近 2^64 個不同元素的基數(shù)芦圾。

但是,因?yàn)?HyperLogLog 只會根據(jù)輸入元素來計算基數(shù)棚蓄,而不會儲存輸入元素本身堕扶,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素梭依,也不能刪除元素,這一點(diǎn)和布隆過濾器非常相似典尾。

此外役拴,HyperLogLog 提供的是不準(zhǔn)確的去重計數(shù)方案,但是可以保證基數(shù)隨統(tǒng)計量單調(diào)遞增钾埂,并且標(biāo)準(zhǔn)誤差達(dá)到 0.81%河闰,這樣的精確度已經(jīng)可以滿足大部分統(tǒng)計需求了科平。

HyperLogLog有三個非常簡單的API:

  • PFADD 將多個值存入指定的HyperLogLog
  • PFCOUNT 獲取指定HyperLogLog的基數(shù)
  • PFMERGE 合并多個HyperLogLog,合并前的結(jié)果和合并后的統(tǒng)計結(jié)果完全一致(取并集)姜性,不存在因合并導(dǎo)致的誤差

HyperLogLog原理

舉一個我們最熟悉的拋硬幣例子瞪慧,出現(xiàn)正反面的概率都是1/2,一直拋硬幣直到出現(xiàn)正面部念,記錄下投擲次數(shù)k弃酌,將這種拋硬幣多次直到出現(xiàn)正面的過程記為一次伯努利過程,對于n次伯努利過程儡炼,我們會得到n個出現(xiàn)正面的投擲次數(shù)值其中最大值記為k?max妓湘,那么可以得到下面結(jié)論:

  • n次伯努利過程的投擲次數(shù)都不大于k?max
  • ?n次伯努利過程,至少有一次投擲次數(shù)等于k?max

那么根據(jù)最大似然估計的原理乌询,我們可以反過來講:進(jìn)行了n次進(jìn)行拋硬幣實(shí)驗(yàn)榜贴,每次分別記錄下第一次拋到正面的拋擲次數(shù)k,那么可以用n次實(shí)驗(yàn)中最大的拋擲次數(shù)k?max來預(yù)估實(shí)驗(yàn)組數(shù)量n:

N=2^K

HyperLog.jpg

基于這個原理妹田,如果我們對存入HyperLogLog的值進(jìn)行hash運(yùn)算唬党,所得hash碼的二進(jìn)制位就可以看做一次伯努利過程。每一位上的0或1就可以看作每次投擲硬幣的結(jié)果鬼佣,那么第一次出現(xiàn)1的位置所對應(yīng)的索引就可以看作kmax驶拱,那么根據(jù)kmax就可以估算存入的數(shù)據(jù)基數(shù)。

但是如果 N 介于 2^K 和 2^(K+1) 之間沮趣,用這種方式估計的值都等于 2^K屯烦,所以用這樣的單一估計偶然性較大,導(dǎo)致誤差較大房铭,因此在實(shí)際的 HyperLogLog 算法中驻龟,會先分桶統(tǒng)計然后計算平均值來消除誤差。這里使用的平均值是調(diào)和平均 (倒數(shù)的平均)缸匪。普通的平均法可能因?yàn)閭€別離群值對平均結(jié)果產(chǎn)生較大的影響翁狐,調(diào)和平均可以有效平滑離群值的影響。

上述經(jīng)過分桶平均后的估計量看似已經(jīng)很不錯了凌蔬,不過通過數(shù)學(xué)分析可以知道這并不是基數(shù)n的無偏估計露懒,因此需要修正成無偏估計。當(dāng)HyperLogLog開始統(tǒng)計數(shù)據(jù)時砂心,統(tǒng)計數(shù)組中大部分位置都是空數(shù)據(jù)懈词,并且需要一段時間才能填滿數(shù)組,這種階段引入一種小范圍修正方法辩诞;當(dāng)統(tǒng)計數(shù)組已滿的時候坎弯,需要統(tǒng)計的數(shù)據(jù)基數(shù)很大,這時候hash空間會出現(xiàn)很多碰撞情況,這種階段引入一種大范圍修正方法抠忘。其中系數(shù)由統(tǒng)計數(shù)組的大小決定撩炊,最終算法用偽代碼可以表示為:

// m 為桶數(shù) b是m的以2為底的對數(shù)
m = 2^b 

if m == 16:
    alpha = 0.673
elif m == 32:
    alpha = 0.697
elif m == 64:
    alpha = 0.709
else:
    alpha = 0.7213/(1 + 1.079/m)

registers = [0]*m   # initialize m registers to 0

###########################################################################
# Construct the HLL structure
for h in hashed(data):
    register_index = 1 + get_register_index( h,b ) # binary address of the rightmost b bits
    run_length = run_of_zeros( h,b ) # length of the run of zeroes starting at bit b+1
    registers[ register_index ] = max( registers[ register_index ], run_length )

##########################################################################
# Determine the cardinality
DV_est = alpha * m^2 * 1/sum( 2^ -register )  # the DV estimate

if DV_est < 5/2 * m: # small range correction
    V = count_of_zero_registers( registers ) # the number of registers equal to zero
    if V == 0:  # if none of the registers are empty, use the HLL estimate
          DV = DV_est
    else:
          DV = m * log(m/V)  # i.e. balls and bins correction

if DV_est <= ( 1/30 * 2^32 ):  # intermediate range, no correction
     DV = DV_est
if DV_est > ( 1/30 * 2^32 ):  # large range correction
     DV = -2^32 * log( 1 - DV_est/2^32)

Redis中的實(shí)現(xiàn)細(xì)節(jié)

數(shù)據(jù)在存入時,value 會被 hash 成 64 位崎脉,即 64 bit 的比特字符串拧咳,前 14 位用來選擇這個 value 的比特串中從右往左第一個 1 出現(xiàn)的下標(biāo)位置數(shù)值要存到那個桶中去,即前 14 位用來分桶囚灼。之所以選 14位 來表達(dá)桶編號是因?yàn)槁嫦ィ至?16384 個桶,而 2^14 = 16384啦撮,剛好可以把每個編號都利用上谭网,不造成浪費(fèi)。假設(shè)一個字符串的前 14 位是:00 0000 0000 0010 (從右往左看) 赃春,其十進(jìn)制值為 2愉择。那么 index 將會被轉(zhuǎn)化后放到編號為 2 的桶。每個桶的index 需要 6 個 bits 來存儲织中,最大可以表示 index=63锥涕,于是總共占用內(nèi)存就是2^14 * 6 / 8 = 12k字節(jié)。

因?yàn)橥暾?value 比特字符串是 64 位形式狭吼,減去 14 后层坠,剩下 50 位,那么極端情況刁笙,出現(xiàn) 1 的位置破花,是在第 50 位,即位置是 50疲吸。此時 index = 50座每。此時先將 index 轉(zhuǎn)為 2 進(jìn)制,它是:110010 摘悴。因?yàn)?6384 個桶中峭梳,每個桶是 6 bit 組成的。剛好 110010 就被設(shè)置到了第 2 號桶中去了蹂喻。請注意葱椭,50 已經(jīng)是最壞的情況,且它都被容納進(jìn)去了口四。那么其他的不用想也肯定能被容納進(jìn)去孵运。

fpadd 的 key 可以設(shè)置多個 value。所以根據(jù)上面的做法蔓彩,不同的 value掐松,會被設(shè)置到不同桶中去踱侣,如果出現(xiàn)了在同一個桶的粪小,即前 14 位值是一樣的大磺,但是后面出現(xiàn) 1 的位置不一樣。那么比較原來的 index 是否比新 index 大探膊。是杠愧,則替換。否逞壁,則不變流济。最終,一個 key 所對應(yīng)的 16384 個桶都設(shè)置了很多的 value 了腌闯,每個桶有一個k_max绳瘟。此時調(diào)用 pfcount 時,按照前面介紹的估算方式姿骏,便可以計算出 key 的設(shè)置了多少次 value糖声,也就是統(tǒng)計值。value 被轉(zhuǎn)為 64 位的比特串分瘦,最終被按照上面的做法記錄到每個桶中去蘸泻。

此外,Redis 對 HyperLogLog 的存儲進(jìn)行了優(yōu)化嘲玫,在計數(shù)比較小時悦施,它的存儲空間采用稀疏矩陣存儲,空間占用很小去团,僅僅在計數(shù)慢慢變大抡诞,稀疏矩陣占用空間漸漸超過了閾值時才會一次性轉(zhuǎn)變成稠密矩陣,才會占用 12k 的空間土陪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昼汗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子旺坠,更是在濱河造成了極大的恐慌乔遮,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件取刃,死亡現(xiàn)場離奇詭異蹋肮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)璧疗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門坯辩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人崩侠,你說我怎么就攤上這事漆魔。” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵改抡,是天一觀的道長矢炼。 經(jīng)常有香客問我,道長阿纤,這世上最難降的妖魔是什么句灌? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮欠拾,結(jié)果婚禮上胰锌,老公的妹妹穿的比我還像新娘。我一直安慰自己藐窄,他們只是感情好资昧,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荆忍,像睡著了一般格带。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上东揣,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天践惑,我揣著相機(jī)與錄音,去河邊找鬼嘶卧。 笑死尔觉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芥吟。 我是一名探鬼主播侦铜,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钟鸵!你這毒婦竟也來了钉稍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤棺耍,失蹤者是張志新(化名)和其女友劉穎贡未,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒙袍,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俊卤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了害幅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片消恍。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖以现,靈堂內(nèi)的尸體忽然破棺而出狠怨,到底是詐尸還是另有隱情约啊,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布佣赖,位于F島的核電站恰矩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏茵汰。R本人自食惡果不足惜枢里,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹂午。 院中可真熱鬧,春花似錦彬碱、人聲如沸豆胸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晚胡。三九已至,卻和暖如春嚼沿,著一層夾襖步出監(jiān)牢的瞬間估盘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工骡尽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遣妥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓攀细,卻偏偏與公主長得像箫踩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谭贪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn) 原創(chuàng)地址:https://blog.csdn.net/hebtu666/article/details/107...
    crossme閱讀 285評論 0 1
  • 2. hyperLogLog ? 如果要實(shí)現(xiàn)這么一個功能: ? 統(tǒng)計 APP或網(wǎng)頁 的一個頁面境钟,每天有多少...
    H三水閱讀 4,490評論 0 1
  • # 前言 ### 為什么我要嘗試寫作技術(shù)書籍 - 一個人年輕時經(jīng)歷的艱難會在未來成為他的財富 # 第一篇 基礎(chǔ)和應(yīng)...
    zhzosh閱讀 623評論 0 0
  • 什么是redis? Redis 本質(zhì)上是一個 Key-Value 類型的內(nèi)存數(shù)據(jù)庫俭识, 整個數(shù)據(jù)庫加載在內(nèi)存當(dāng)中進(jìn)行...
    燒餅丨灬閱讀 1,315評論 0 0
  • 金九銀十即將到來慨削,整理了20道經(jīng)典Redis面試題,希望對大家有幫助套媚。 1. 什么是Redis缚态?它主要用來什么的?...
    學(xué)編程的小屁孩閱讀 1,552評論 0 1