系列文章目录
文章目录
一、HyperLogLog是什么?
HyperLogLog并不是一种新的数据结构(实际类型为字符串类 型),而是一种基数估算算法。所谓基数估算,就是估算在一批数据中,不重复元素的个数有多少。通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。
二、HyperLogLog应用场景
根据HyperLogLog的特性可知,该技术可应用于一些要求统计基数且准确性可相对低的需求。由于HyperLogLog存在误差(官网给出的说法是0.81%的误差),因此当要使用HyperLogLog来做基数计算的时候,需要考虑业务需求是否能接受轻微的误差。当然这些误差也换来了内存的损耗少的好处。
因此,HyperLogLog 主要的应用场景就是进行基数统计:
- 统计某用户的某文章的被阅读次数,可按每天、每月、每年等自定义时间进行统计。
- 对于 Google 主页面而言,同一个账户可能会访问 Google 主页面多次。于是,在诸多的访问流水中,如何计算出 Google 主页面每天被多少个不同的账户访问过就是一个重要的问题。那么对于 Google 这种访问量巨大的网页而言,其实统计出有十亿 的访问量或者十亿零十万的访问量其实是没有太多的区别的,因此,在这种业务场景下,为了节省成本,其实可以只计算出一个大概的值,而没有必要计算出精准的值。
- 统计注册 IP 数
- 统计每日访问 IP 数
- 统计页面实时 UV 数
- 统计在线用户数
- 统计用户每天搜索不同词条的个数
- …更多
三、Redis中使用HyperLogLog
3.1 PFADD
PFADD
最早可用版本:2.8.9
时间复杂度:O(1)
用法:PFADD key element [element …]
将参数中的元素都加入指定的HyperLogLog数据结构中,这个命令会影响基数的计算。如果执行命令之后,基数估计改变了,就返回1;否则返回0。如果指定的key不存在,那么就创建一个空的HyperLogLog数据结构。该命令也支持不指定元素而只指定键值,如果不存在,则会创建一个新的HyperLogLog数据结构,并且返回1;否则返回0
代码示例:
redis> PFADD databases "Redis" "MongoDB" "MySQL"
(integer) 1
redis> PFCOUNT databases
(integer) 3
redis> PFADD databases "Redis" # Redis 已经存在,不必对估计数量进行更新
(integer) 0
redis> PFCOUNT databases # 元素估计数量没有变化
(integer) 3
redis> PFADD databases "PostgreSQL" # 添加一个不存在的元素
(integer) 1
redis> PFCOUNT databases # 估计数量增一
4
集成Springboot使用:
/**
* 将参数中的元素都加入指定的HyperLogLog数据结构中,这个命令会影响基数的计算。
* 如果执行命令之后,基数估计改变了,就返回1;否则返回0。如果指定的key不存在,
* 那么就创建一个空的HyperLogLog数据结构。该命令也支持不指定元素而只指定键值,
* 如果不存在,则会创建一个新的HyperLogLog数据结构,并且返回1;否则返回0
* @param key key值
* @param value value
* @return 如果 HyperLogLog 的内部被修改了,那么返回 1,否则返回 0
*/
public Long pfAdd(String key, Object... value){
return redisTemplate.opsForHyperLogLog().add(key, value);
}
3.2 PFCOUNT
PFCOUNT
最早可用版本:2.8.9
时间复杂度:O(1),对于多个比较大的key的时间复杂度是O(N)
用法:PFCOUNT key [key …]
对于单个key,该命令返回的是指定key的近似基数,如果变量不存在,则返回0。
对于多个key,返回的是多个HyperLogLog并集的近似基数,它是通过将多个HyperLogLog合并为一个临时的HyperLogLog,然后计算出来的。
HyperLogLog可以用很少的内存来存储集合的唯一元素。(每个HyperLogLog只有12K加上key本身的几个字节)
HyperLogLog的结果并不精准,错误率大概在0.81%。
需要注意的是:该命令会改变HyperLogLog,因此使用8个字节来存储上一次计算的基数。所以,从技术角度来讲,PFCOUNT是一个写命令
性能问题
即使理论上处理一个存储密度大的HyperLogLog需要花费较长时间,但是当指定一个key时,PFCOUNT命令仍然具有很高的性能。这是因为PFCOUNT会缓存上一次结算的基数,而多数PFADD命令不会更新寄存器。所以才可以达到每秒上百次请求的效果。
当处理多个key时,最耗时的一步是合并操作。而通过计算出来的并集的基数是不能缓存的。所以多个key的处理速度一般在毫秒级。
代码示例:
redis> PFADD databases "Redis" "MongoDB" "MySQL"
(integer) 1
redis> PFCOUNT databases
(integer) 3
redis> PFADD databases "Redis" # Redis 已经存在,不必对估计数量进行更新
(integer) 0
redis> PFCOUNT databases # 元素估计数量没有变化
(integer) 3
redis> PFADD databases "PostgreSQL" # 添加一个不存在的元素
(integer) 1
redis> PFCOUNT databases # 估计数量增一
4
集成Springboot使用:
/**
* 对于单个key,该命令返回的是指定key的近似基数,如果变量不存在,则返回0。
*
* 对于多个key,返回的是多个HyperLogLog并集的近似基数,它是通过将多个HyperLogLog合并为一个临时的HyperLogLog,然后计算出来的。
*
* HyperLogLog可以用很少的内存来存储集合的唯一元素。(每个HyperLogLog只有12K加上key本身的几个字节)
*
* HyperLogLog的结果并不精准,错误率大概在0.81%。
*
* 需要注意的是:该命令会改变HyperLogLog,因此使用8个字节来存储上一次计算的基数。所以,从技术角度来讲,PFCOUNT是一个写命令
* @param key key值(可多个)
* @return 添加的唯一元素的近似数量.
*/
public Long pfCount(String... key){
return redisTemplate.opsForHyperLogLog().size(key);
}
3.3 PFMERGE
最早可用版本:2.8.9
时间复杂度:O(N),N是要合并的HyperLogLog的数量
用法:PFMERGE destkey sourcekey [sourcekey …]
合并多个HyperLogLog,合并后的基数近似于合并前的基数的并集(observed Sets)。计算完之后,将结果保存到指定的key。
除了这三个命令,我们还可以像操作String类型的数据那样,对HyperLogLog数据使用SET和GET命令。
代码示例:
redis> PFADD nosql "Redis" "MongoDB" "Memcached"
(integer) 1
redis> PFADD RDBMS "MySQL" "MSSQL" "PostgreSQL"
(integer) 1
redis> PFMERGE databases nosql RDBMS
OK
redis> PFCOUNT databases
(integer) 6
集成Springboot使用:
/**
*
* 将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集.
*
* 合并得出的 HyperLogLog 会被储存在目标变量(第一个参数)里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的.
* @param key key值
* @param otherKey 另外的key值(集合)多个
* @return 返回并集个数
*/
public Long pfMerge(String key,String... otherKey){
return redisTemplate.opsForHyperLogLog().union(key, otherKey);
}
3.4 测试demo
String key1 = "key1";
String key2 = "key2";
String key3 = "ley3";
Long addCount1 = redisUtils.pfAdd(key1, "uuid-1", "uuid-2", "uuid-3");
Long addCount2 = redisUtils.pfAdd(key2, "uuid-2", "uuid-3", "uuid-4");
System.out.println("addCount1:"+ addCount1);
System.out.println("addCount2:"+ addCount2);
//获取键值为key1的基数
Long count1 = redisUtils.pfCount(key1);
//获取键值为key2的基数
Long count2 = redisUtils.pfCount(key2);
//获取键值为key1、key2的基数
Long count12 = redisUtils.pfCount(key2,key1);
System.out.println("count1 :"+count1);
System.out.println("count2 :"+count2);
System.out.println("count12 :"+count12);
//合并key1与key2的基数到key3
Long addCount3 = redisUtils.pfMerge(key3, key1, key2);
System.out.println("addCount3:"+addCount3);
//获取键值为key3的基数
Long count3 = redisUtils.pfCount(key3);
System.out.println("count3 :"+count3);
结果为:
addCount1:1
addCount2:1
count1 :3
count2 :3
count12 :4
addCount3:4
count3 :4
总结
HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行 数据结构选型时只需要确认如下两条即可:
- 只为了计算独立总数,不需要获取单条数据。
- 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大 的优势。