1. 业务场景:
HyperLogLog常用于大数据量的统计,比如页面访问量统计或者用户访问量统计。
- 需求:要统计一个页面的访问量(PV)
- 方案:直接用redis计数器或者直接存数据库都可以
- 需求:要统计一个页面的用户访问量(UV),即:一个用户一天内如果访问多次的话,也只能算一次
- 方案:可能会想到用SET集合来做,因为SET集合是有去重功能的,key存储页面对应的关键字,value存储对应userId
- 需求:假如有几千万访问量,为了统计一个访问量,要频繁创建SET集合对象。
- 方案:针对大访问量需要进行统计的问题,redis实现了一种HyperLogLog算法。
2. 简介
Redis HyperLogLog是用来做技术统计的算法,优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的,并且是很小的。
在redis中,每个hyperLogLog键只需要花费12KB内存,记可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存越多的集合形成鲜明对比。
但是,由于HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本书,所以HyperLogLog不能像集合那样,返回输入的各个元素。
2.1 什么是基数
比如数据集{1,3,5,7,5,7,8}的基数集为{1,3,5,7,8},基数(不重复元素个数)为5。基数就是在误差可接受范围内,快速计算基数。
3. 应用场景:
统计注册 IP 数
统计每日访问 IP 数
统计页面实时 UV 数
统计在线用户数
统计用户每天搜索不同词条的个数
说明:基数不大,数据量不大就用不上,会有点大材小用浪费空间
有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
和bitmap相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
4. 总结:
- HyperLogLog是一种算法,并非redis独有
- 目的是做基数统计,故不是集合,不会保存元数据,只记录数量而不是数值。
- 耗空间极小,支持输入非常体积的数据量
- 核心是基数估算算法,主要表现为计算时内存的使用和数据合并的处理。最终数值存在一定误差
- redis中每个hyperloglog key占用了12K的内存用于标记基数
- pfadd命令并不会一次性分配12k内存,而是随着基数的增加而逐渐增加内存分配;而pfmerge操作则会将sourcekey合并后存储在12k大小的key中,这由hyperloglog合并操作的原理(两个hyperloglog合并时需要单独比较每个桶的值)可以很容易理解。
- 误差说明:基数估计的结果是一个带有 0.81% 标准错误(standard error)的近似值。是可接受的范围
- Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间
- HyperLogLog算法一开始就是为了大数据量的统计而发明的,所以很适合那种数据量很大,然后又没要求不能有一点误差的计算,HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,不过这对于页面用户访问量是没影响的,因为这种统计可能是访问量非常巨大,但是又没必要做到绝对准确,访问量对准确率要求没那么高,但是性能存储方面要求就比较高了,而HyperLogLog正好符合这种要求,不会占用太多存储空间,同时性能不错。
5. 命令:
1.pfadd:将除了第一个参数以外的参数存储到以第一个参数为变量名的HyperLogLog结构中
- 格式:
PFADD key element [element ...]
- 这个命令的一个副作用是它可能会更改这个HyperLogLog的内部来反映在每添加一个唯一的对象时估计的基数(集合的基数).
- 如果一个HyperLogLog的估计的近似基数在执行命令过程中发了变化, PFADD 返回1,否则返回0,如果指定的key不存在,这个命令会自动创建一个空的HyperLogLog结构(指定长度和编码的字符串).
- 如果在调用该命令时仅提供变量名而不指定元素也是可以的,如果这个变量名存在,则不会有任何操作,如果不存在,则会创建一个数据结构(返回1)
- 返回值:如果 HyperLogLog 的内部被修改了,那么返回 1,否则返回 0
- 复杂度:O(1)
redis> PFADD hll a b c d e f g
(integer) 1
redis> PFCOUNT hll
(integer) 7
2.pfcount:返回存储在HyperLogLog结构体的该变量的近似基数
- 格式:
PFCOUNT key [key ...]
- 当参数为一个key时,返回存储在HyperLogLog结构体的该变量的近似基数,如果该变量不存在,则返回0.
- 当参数为多个key时,返回这些HyperLogLog并集的近似基数,这个值是将所给定的所有key的HyperLoglog结构合并到一个临时的HyperLogLog结构中计算而得到的.
- HyperLogLog可以使用固定且很少的内存(每个HyperLogLog结构需要12K字节再加上key本身的几个字节)来存储集合的唯一元素.
- 返回的可见集合基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值.
- 例如为了记录一天会执行多少次各不相同的搜索查询, 一个程序可以在每次执行搜索查询时调用一次PFADD, 并通过调用PFCOUNT命令来获取这个记录的近似结果.
- 注意: 这个命令的一个副作用是可能会导致HyperLogLog内部被更改,出于缓存的目的,它会用8字节的来记录最近一次计算得到基数,所以PFCOUNT命令在技术上是个写命令
- 返回值:添加的唯一元素的近似数量
- 复杂度:O(1) with every small average constant times when called with a single key. O(N) with N being the number of keys, and much bigger constant times, when called with multiple keys
redis> PFADD hll foo bar zap
(integer) 1
redis> PFADD hll zap zap zap
(integer) 0
redis> PFADD hll foo bar
(integer) 0
redis> PFCOUNT hll
(integer) 3
redis> PFADD some-other-hll 1 2 3
(integer) 1
redis> PFCOUNT hll some-other-hll
(integer) 6
性能
当调用PFCOUNT命令时指定一个key为参数,性能表现很好,甚至和处理一个HyperLogLog所需要的时间一样短.这可能和PFCOUNT命令能够直接使用缓存的的估计基数有关,大多数的PFADD也不会更新任何寄存器,所以这个值也很少被更改.理论上能达到每秒几百次操作.
当调用PFCOUNT命令时指定多个key,由于要在多个HperLogLog结构中执行一比较慢合并操作,而且这个通过并集计算得到的基数是不能够被缓存, PFCOUNT命令还要消耗毫秒量级的时间来进行多个key的并集操作,消耗的时间会比较长一些,所以不要滥用这种多个key的方式.
使用者需要明白这个命令来处理1个key和多个key执行的语义是不同的,并且执行的性能也不相同.
更多的信息请参考这篇文章. 源代码 hyperloglog.c文件也很简单易理解, 包含了稀松与密集两种实现的编码.
3.pfmerge:将多个 HyperLogLog 合并(merge)为一个 HyperLogLog
- 格式:
PFMERGE destkey sourcekey [sourcekey ...]
- 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集.
- 合并得出的 HyperLogLog 会被储存在目标变量(第一个参数)里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的
- 返回值:OK
- 复杂度:O(N) to merge N HyperLogLogs, but with high constant times
redis> PFADD hll1 foo bar zap a
(integer) 1
redis> PFADD hll2 a b c foo
(integer) 1
redis> PFMERGE hll3 hll1 hll2
OK
redis> PFCOUNT hll3
(integer) 6