HyperLogLog原理与在Redis中的使用
Redis-HyperLogLog
基于HyperLogLog算法,使用極小的空間完成巨量運(yùn)算
Redis 中HyperLogLog 基本使用
常用命令
python 操作Redis HyperLogLog
from MyRedis.RedisTool import RedisToolclass RedisHLL:def __init__(self):self._conn = RedisTool.redis_connection("127.0.0.1", 8100, "redis")def hll_test(self):self._conn.pfadd('test', "junebao", "python", "redis", "hyperloglog", "java")count = self._conn.pfcount("test")print(count)if __name__ == '__main__':RedisHLL().hll_test() # 5HyperLogLog 算法原理
特點(diǎn):
- 能使用極少的內(nèi)存來統(tǒng)計(jì)巨量的數(shù)據(jù),在Redis中的HyperLogLog只需要12k內(nèi)存就能統(tǒng)計(jì) 2642^{64}264
- 計(jì)數(shù)存在一定的誤差,但誤差率整體較低,標(biāo)準(zhǔn)誤差為 0.81%
- 可以設(shè)置輔助計(jì)算因子減小誤差
LogLog 簡(jiǎn)介
HyperLogLog 其實(shí)是 LogLog 算法的改進(jìn)版,Loglog源于著名的伯努利實(shí)驗(yàn)。
這個(gè)實(shí)驗(yàn)是這樣的:隨機(jī)拋一枚硬幣,那么正面朝上和反面朝上的概率都應(yīng)該是 50% ,那么如果一直重復(fù)拋硬幣,直到出現(xiàn)正面朝上,就記作1次伯努利實(shí)驗(yàn)。
對(duì)于單個(gè)一次伯努利實(shí)驗(yàn),拋硬幣的次數(shù)是不確定的,有可能第一次就正面朝上,那這1次就被記為1次伯努利實(shí)驗(yàn),也有可能拋了10次才出現(xiàn)正面朝上,那這10次才會(huì)被記作1次伯努利實(shí)驗(yàn)。
假設(shè)做了n次伯努利實(shí)驗(yàn),第一次實(shí)驗(yàn)拋了 k1k_1k1? 次硬幣, 第二次拋了 k2k_2k2? 次硬幣,那么第 n 次實(shí)驗(yàn)就拋了 knk_nkn? 次硬幣。在 [k1?kn][k_1 -k_n][k1??kn?] 之間,就必然存在一個(gè)最大值 kmaxk_{max}kmax? , kmaxk_{max}kmax?的意義就是在這一組伯努利實(shí)驗(yàn)中,出現(xiàn)正面朝上需要的最多的拋擲次數(shù)。結(jié)合極大似然估計(jì)方法得到伯努利實(shí)驗(yàn)的次數(shù) nnn 和這個(gè)最大值 kmaxk_{max}kmax? 存在關(guān)系: n=2kmaxn = 2^{k_{max}}n=2kmax?
例如:實(shí)驗(yàn)0和1表示硬幣的正反,一輪做五次實(shí)驗(yàn),某輪伯努利實(shí)驗(yàn)的結(jié)果為
# 第一次 001 # 第二次 01 # 第三次 1 # 第四次 0001 # 第五次 001那么這一輪伯努利實(shí)驗(yàn)的 kmax=4k_{max}=4kmax?=4 ,按照上面的公式應(yīng)該得到 5=245=2^45=24,這個(gè)誤差顯然太過巨大,我們可以增加某一輪實(shí)驗(yàn)的次數(shù),用python模擬一下
import randomclass BernoulliExp:def __init__(self, freq: int):self.freq = freqself.option = [0, 1]def run(self):k_max = 0for i in range(self.freq):num = 0while True:num += 1result = random.choice(self.option)if result == 1:break# print(f"第{i}次伯努利實(shí)驗(yàn),拋了{(lán)num}次硬幣")if num > k_max:k_max = numreturn k_maxif __name__ == '__main__':be = BernoulliExp(5000)k_max = be.run()print(f"k_max={k_max}")通過測(cè)試,當(dāng)每一輪進(jìn)行5000次伯努利實(shí)驗(yàn)時(shí),進(jìn)行五輪,kmaxk_{max}kmax?分別為 12, 12, 14, 11, 15,誤差仍舊很大,所以我們可以進(jìn)行多輪伯努利實(shí)驗(yàn),求kmaxk_{max}kmax?的平均值,用python模擬一下
import randomclass BernoulliExp:def __init__(self, freq: int, rounds: int, num: int):"""Args:freq: int,每輪進(jìn)行多少次實(shí)驗(yàn)rounds: k_max 對(duì)多少輪實(shí)驗(yàn)求平均num: 進(jìn)行多少次這樣的實(shí)驗(yàn)(求誤差)"""self.freq = freqself.option = [0, 1]self.rounds = roundsself.number_of_trials = numdef _run_one_round(self):k_max = 0for i in range(self.freq):num = 0while True:num += 1result = random.choice(self.option)if result == 1:break# print(f"第{i}次伯努利實(shí)驗(yàn),拋了{(lán)num}次硬幣")if num > k_max:k_max = numreturn k_maxdef get_k_max(self):sum_k_max = 0for i in range(self.rounds):sum_k_max += self._run_one_round()return sum_k_max / self.roundsdef deviation(self):dev = 0for i in range(self.number_of_trials):k_max = self.get_k_max()print(f"第{i}次:k_max = {k_max}")dev += (2 ** k_max) - self.freqreturn dev/self.number_of_trialsif __name__ == '__main__':be = BernoulliExp(6, 16384, 5)dev = be.deviation()print(f"誤差:{dev}") 第0次:k_max = 4.03546142578125 第1次:k_max = 4.034423828125 第2次:k_max = 4.05010986328125 第3次:k_max = 4.02423095703125 第4次:k_max = 4.045654296875 誤差:10.427087015403654這時(shí)誤差依舊非常大,但我們發(fā)現(xiàn) kmaxk_{max}kmax?卻浮動(dòng)在4.038上下,這就說明nnn和 kmaxk_{max}kmax? 之間的關(guān)系確實(shí)存在,但公式前面還應(yīng)該有一個(gè)常數(shù)項(xiàng),原公式應(yīng)該是 n=α?2kmaxn = \alpha · 2^{k_{max}}n=α?2kmax?
通過簡(jiǎn)單計(jì)算,把 α\alphaα設(shè)為 0.36520.36520.3652:
第0次:k_max = 4.055908203125 第1次:k_max = 4.0262451171875 第2次:k_max = 4.03045654296875 第3次:k_max = 4.04534912109375 第4次:k_max = 4.048095703125 誤差:0.01269833279264585這里0.3652是用n=6n=6n=6計(jì)算出來的,但當(dāng)n取其他值時(shí),這個(gè)因子也能基本將相對(duì)誤差控制在0.1以內(nèi)。
上面的公式,便是LogLog的估算公式
DVLL=constant?m?2R ̄DV_{LL} = constant * m * 2 ^ {\overline{R}}DVLL?=constant?m?2R
其中 DVLLDV_{LL}DVLL?就是n,constant就是調(diào)和因子, m是實(shí)驗(yàn)輪數(shù),R ̄\overline{R}R 是 kmaxk_{max}kmax?的平均值。
而 HyperLogLog和LogLog的區(qū)別就是使用調(diào)和平均數(shù)計(jì)算kmaxk_{max}kmax?,這樣如果計(jì)算的數(shù)值相差較大,調(diào)和平均數(shù)可以較好的反應(yīng)平均水平,調(diào)和平均數(shù)的計(jì)算方式為:
Hn=n∑i=1n1xiH_n = \frac{n}{\sum_{i=1} ^ n \frac{1}{x_i}}Hn?=∑i=1n?xi?1?n?
所以 HyperLogLog 的公式就可以寫為
DVHLL=const?m?m∑j=1m12RjDV_{HLL} = const * m * \frac{m}{\sum_{j=1} ^ m \frac{1}{2^{R_j}}}DVHLL?=const?m?∑j=1m?2Rj?1?m?
在Redis中的實(shí)現(xiàn)方法
如果我們我們可以通過kmaxk_{max}kmax?來估計(jì)nnn,那同樣的,對(duì)于一個(gè)比特串,我們就可以按照這個(gè)原理估算出里面1的個(gè)數(shù),例如在
統(tǒng)計(jì)一個(gè)頁面每日的點(diǎn)擊量(同一用戶不重復(fù)計(jì)算)
要實(shí)現(xiàn)這個(gè)功能,最簡(jiǎn)單的辦法就是維持一個(gè)set,每當(dāng)有用新戶訪問頁面,就把ID加入集合(重復(fù)訪問的用戶也不會(huì)重復(fù)加),點(diǎn)擊量就是集合的長(zhǎng)度,但這樣做最大的問題就是會(huì)浪費(fèi)很多空間,如果一個(gè)用戶ID占8字節(jié),加入有一千萬用戶,那就得消耗幾十G的空間,但Redis只用了12k就完成了相同的功能。
首先,他把自己的12k劃分為 16834 個(gè) 6bit 大小的 “桶”,這樣每個(gè)桶所能表示的最大數(shù)字為 1111(2)=63{1111}_{(2)} = 631111(2)?=63, 在存入時(shí),把用戶ID作為Value傳入,這個(gè)value會(huì)被轉(zhuǎn)換為一個(gè)64bit的比特串,前14位用來選擇這個(gè)比特串從右往左看,第一次出現(xiàn)1的下標(biāo)要儲(chǔ)存的桶號(hào)。
例如一個(gè)value經(jīng)過Hash轉(zhuǎn)換后的比特串為
[0000 0000 0000 1100 01]01 0010 1010 1011 0110 1010 0111 0101 0110 1110 0110 0100這個(gè)比特串前14位是 110001(2){110001}_{(2)}110001(2)?,轉(zhuǎn)換成10進(jìn)制也就是49,而它從右往左看,第3位是1,所以3會(huì)被放到49桶中(首先要看49桶中原來的值是不是小于3,如果比3小,就用3替換原來的,否則不變,【因?yàn)橥爸写娴氖?span id="ozvdkddzhkzd" class="katex--inline">kmaxk_{max}kmax?】), kmaxk_{max}kmax?在這里最大也只能是64,用6bit肯定夠用。
這樣不管有多少用戶訪問網(wǎng)站,存儲(chǔ)的只有這12k的數(shù)據(jù),訪問量越多,kmaxk_{max}kmax? 越大,然后根據(jù)HyperLogLog公式,就可以較精確的估計(jì)出訪問量。(一個(gè)桶可以看作一輪伯努利實(shí)驗(yàn))
修正因子
constant 并不是一個(gè)固定的值,他會(huì)根據(jù)實(shí)際情況而被分支設(shè)置,如: P=log?2mP = \log_2 mP=log2?m
m 是分桶數(shù)
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; }參考
https://www.cnblogs.com/linguanh/p/10460421.html#commentform
https://chenxiao.blog.csdn.net/article/details/104195908
總結(jié)
以上是生活随笔為你收集整理的HyperLogLog原理与在Redis中的使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2016matlab打开toolbox,
- 下一篇: redis配置主从没效果_跟我一起学Re