vlambda博客
学习文章列表

Redis站点流量统计HyperLogLog

在我们做站点流量统计的时候一般会统计页面UV(独立访客:unique visitor)和PV(即页面浏览量:page view),那么我们最常见的处理方式就是用户点击一次就插入一条数据到数据库,统计的时候通过查询表来达到统计流量的效果。

那么我们如果是通过redis来处理,我们可以使用string类型然后自增计数即可达到统计PV,统计UV可以使用set,每个用户id是唯一的可以放到这个集合里,统计的时候只需要执行scard获取集合大小即可。

这两种方式都是可以实现站点的流量统计,但是如果说当站点流量非常大每天几千万次的访问量,那么数据库可能就要分表分库了,redis添加足够多的数据后内存消耗也是非常惊人的,总的来说这样做是不划算的,那么我们的redis有一种专门处理这样数据的算法,HyperLogLog。

HyperLogLog基本使用方式

HyperLogLog提供了两个指令pfadd和pfcount,根据字面意思很好理解,一个是增加计数,一个是获取计数。pfadd和set集合的sadd的用法是一样的,pfcount和scard的用法是一样的,直接获取计数值。

HyperLogLog还提供一个合并的指令pfmerge,用于将两个pf累加形成一个新的pf,如果说我们需要将每个页面的流量合并,那么我们这个指令大有用处。

> pfadd user mango(integer) 1> pfadd user zhangsan(integer) 1> pfadd user lisi(integer) 1> pfadd user mango     #重复则不计数(integer) 0> pfcount user(integer) 3
> pfadd paper mango(integer) 1> pfadd paper zhangsan(integer) 1> pfmerge pv user paper    #合并OK> pfcount pv(integer) 3

注意:

  1. HyperLogLog的计数统计是有一定的误差的,误差最大在1%以下。

  2. HyperLogLog数据结构需要占据12KB的存储空间,所以我们在使用的时候得注意,如果数据量非常小我们可以选择其他方式,但是如果是数据量非常大,那么我们这个数据结构就非常有价值了。但是我们也不要太过于担心它的消耗,一开始初始的时候是没有这么大的,在计数比较小时他是采用稀疏矩阵来进行存储,空间占用率还是很小的,只有到达一定阈值后才会转变成稠密矩阵空间才会到达12KB。

命令pf前缀的由来

我们使用这个HyperLogLog命令的时候有点奇怪的感觉,因为我们使用hash的时候都是h开头hadd,set都是sadd,zset都是z开头,那么我们的HyperLogLog为啥是个pf呢?HyperLogLog这个数据结构的发明人Philippe Flajolet, pf 是他的名字的首字母缩写。
HyperLogLog实现原理

HyperLogLog实际上不会存储每个元素的值,它使用的是概率算法,通过存储元素的hash值的第一个1的位置,来计算元素数量。

Redis站点流量统计HyperLogLog

我们第一次产生的随机值获取到它的二进制,从右往左计数连续0的个数,每次获取这个数出现1和0的概率都是1/2,k在任意回合出现的概率即为(1/2)^k,因此可以推测n=2^k,但是一般来讲n的值会在2^k和2^k之间徘徊,为了要让这个算法更加准确,于是引入了桶的概念,计算m个桶的加权平均值,这样就能得到比较准确的答案了(实际上还要进行其他修正)。最终的公式如图

其中m是桶的数量,const是修正常数,它的取值会根据m而变化。p=log2m

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;}

有兴趣看源码的同学可以去github上查看redis HyperLogLog的代码实现,考虑到篇幅太多,读起来紧张我们这里就不贴源代码了

HyperLogLog相关的命令有三个:

pfadd hll ele:将ele添加进hll的基数计算中。流程:

  1. 先对ele求hash(使用的是一种叫做MurMurHash的算法) 

  2. 将hash的低14位(因为总共有2的14次方个桶)作为桶的编号,选桶,记桶中当前的值为count

  3. 从的hash的第15位开始数0,假设从第15位开始有n个连续的0(即前导0)

  4. 如果n大于count,则把选中的桶的值置为n,否则不变

pfcount hll:计算hll的基数。就是使用上面给出的DV公式根据桶中的数值,计算基数

pfmerge hll3 hll1 hll2:将hll1和hll2合并成hll3。合并方法是将所有的HyperLogLog对象合并到一个名为max的对象中,max采用的是密集存储结构,如果被合并的对象也是密集存储结构,则循环比较每一个计数值,将大的那个存入max。

Redis的所有HyperLogLog结构都是固定的16384个桶(2的14次方),并且有两种存储格式:

  • 稀疏格式:HyperLogLog算法在刚开始的时候,大多数桶其实都是0,稀疏格式通过存储连续的0的数目,而不是每个0存一遍,大大减小了HyperLogLog刚开始时需要占用的内存

  • 紧凑格式:用6个bit表示一个桶,需要占用12KB内存

pf 的内存占用为什么是 12k?
我们在上面的算法中使用了1024个桶进行独立计数,不过在Redis的HyperLogLog实现中用到的是16384个桶,也就是2^14,每个桶的maxbits需要6个bits来存储,最大可以表示maxbits=63,于是总共占用内存就是2^14 * 6/8 = 12k 字节。



一名正在抢救的coder

笔名:mangolove