统计UV的实现方式
- Set
- bitmap
- Redis杀器HypeLogLog
1. Set实现
为了统计页面UV,及不重复的所有用户,我们可以建立一个Set的结构,存储所有访问页面的用户ID,根据Set的特性,我们可以保证每个ID只出现一次,从而得出UV,但这样会存在内存问题,假设每个ID为一个Integer,4字节,那么存储10亿个用户需要4G的内存空间,如果多统计几个页面,这得多少空间,太浪费了。
2. bitmap实现
用bitmap的好处是一个字节可以存储8个数,如果存储10亿个ID,1000000000/8/1024/1024 = 119.2M,相对于Set的实现,节省了40倍的空间,可能对一般小公司,足以,然而,对于海量数据怎么办。
3.1 HypeLogLog介绍
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog是用来做基数统计的算法,**HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。**在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
3.2 HypeLogLog原理
HypeLogLog是概率学算法,只要数据量足够大,统计的结果就会无限的趋近于精确,搞懂原理前,我们先学学基本概率知识。
3.2.1 伯努利试验
伯努利试验指的是单次事件,而这次事件的结果是两个可能性结果中的一个。这样的事件都可以表达成“是或否”(“yes or no”)问题。例如:
- 硬币掉落后是人头朝上吗?
- 刚出生的小孩是个女孩吗?
因此结果称为“成功”和“失败”,而结果不应该照字面推断。伯努利试验的例子包括:
- 抛硬币。在这里,正面(人头面)通常表示成功而反面(刻字面)表示失败。一枚均匀硬币,按照定义成功机会是一半p=1/2。
- 掷骰子,在这个例子里我们称六是"成功"而其他都是"失败",p=1/6。
在数学上,这样的试验是以随机变量为模型,而随机变量只能有两个值:0和1,1被认为是"成功"。在单次的伯努利试验中,如果 p 是成功的概率,那么将呈现伯努利分布,此时随机变量的期望值就是 p。
一个伯努利过程(Bernoulli process)是由重复出现独立但是相同分布的伯努利试验组成,例如抛硬币十次,而此时呈现之结果将呈现二项分布。
3.2.2 原理
概率告诉我们,如果我们进行无数次的伯努利试验,那么,最终成功的机率就是概率,也就是说,如果我们抛硬币无数次,人头朝上的概率就是1/2,如果掷骰子无数次,6的概率就是1/6.那么,如果你告诉我说:你最近掷骰子,出现了一次如下情况,连续3次都是6,那么我计算本次事件发生的概率为(1/6)^3,即1/216,那么我告诉你,你可能掷骰子的次数是216次,因为出现该事件的概率是1/216,而你恰恰只出现了一次这样的事件。
HypeLogLog基本就是这样的一个原理,但这样做误差可能很大,比如你就掷骰子3次,3次都为6,我却说你掷了216,为了解决误差问题,我让你同时掷3个骰子,掷很多次,最后你告诉我每个骰子连续出现6的最大次数,比如分别为3, 1, 2,按照概率我计算得到216次,6次,36次,最后我取加权平均数3/(1/216 + 1/6 + 1/36)=15次,15*3=45,这样相对只用一个骰子进行试验误差就会小一些,道理很简单,就是放大样本量,比如有一些调研问卷,只让一个年龄段的人填结论总是有误差的,于是就需要多个年龄段的人填,以此减小误差。
3.2.3 HypeLogLog和伯努利
上面掷骰子的例子中,成功概率为1/6,在HypeLogLog中,它是这么计算的,一个值V,经过hash(V)后得到了hv,hv换算为2进制,每个位置出现0和1的概率是相同的,为1/2,那么每个数都可以当做是一轮伯努利试验,把从低位开始,出现联系个0的次数记为K(和上面连续出现6的次数是一个道理),然后根据K估计样本量大小。为减小误差,就多进行几轮试验,于是HypeLogLog出现了分桶逻辑。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXd0sl6p-1599968649440)(7198623A5C94442F9BC90CB524C2FA63)]](https://i-blog.csdnimg.cn/blog_migrate/56a6b30498974e1d89c1c1b767fef54d.png#pic_center)
在HypeLogLog中,一个值会被Hash为一个64位的二进制数,Hash结果的前14位进行分桶,共有2^14=16384个桶,后面的50位作为一轮伯努利试验,那么最坏情况下,连续50次都出现了0,那么在对应的桶中记下50。50用二进制表示:110010,为6位,因此每个桶用6bit进行存储。
举个栗子:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q681Gc3D-1599968649441)(2889A9C37C014032BCF97FC3D884A54F)]](https://i-blog.csdnimg.cn/blog_migrate/9b2b8d535976d97f9d1eb5c94087b755.png#pic_center)
最后计算公式:
DV_{HLL}=const*m*{m\over{\sum_{i=1}^m{1\over 2^{R_i}}}}
const为常数,m为桶的数量,最后一部分为根据每个桶里的值计算的加权平均数,即常量*桶*加权平均数,这里的2是因为每个位置为0的概率都为1/2.
这就是hypeLogLog的原理。
3.2.4 空间计算
共2^14 =16834个桶,每个桶用6bit存数据,每个哈希值64位,因此可以用2^14 *6/8/1024=12K就可以存储2^64 个数据。
4.HLL操作
PFADD key value [value...] // 将指点元素加入HLL
PFCOUNT key [key...]返回给的key的基数概率估计值
PFMERGE destKey sourceKey [sourceKye...]合并多个HLL到一个Key为destKey的HLL中
参考文献
https://juejin.im/post/6844903785744056333
http://content.research.neustar.biz/blog/hll.html
https://zh.wikipedia.org/wiki/%E4%BC%AF%E5%8A%AA%E5%88%A9%E5%88%86%E5%B8%83
https://zh.wikipedia.org/wiki/%E4%BC%AF%E5%8A%AA%E5%88%A9%E8%A9%A6%E9%A9%97
http://antirez.com/news/75

Redis的HypeLogLog是一种用于基数估算的概率数据结构,它在处理大量数据时仅占用固定且较小的内存。通过伯努利试验原理,HypeLogLog将64位哈希值的高位用于分桶,低位进行伯努利试验,以减少存储和计算误差。每个桶使用6bit存储最大连续0的次数,进而估算基数。尽管它不保存原始数据,但能有效统计接近2^64个不同元素的基数。
3276

被折叠的 条评论
为什么被折叠?



