1、集合元素统计的第一个场景:聚合统计
当你需要对多个集合进行聚合计算时,Set 类型会是一个非常不错的选择。
不过,我要提醒你一下,这里有一个潜在的风险。
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。所以,我给你分享一个小建议:你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。
SetOperations<String,String> setOperations = redisTemplate.opsForSet();
setOperations.add("user:id","zhangsan1");
setOperations.add("user:id","lisi2");
setOperations.add("user:id","wangwu34");
setOperations.add("user:id","liuliu44");
setOperations.add("user:id","qiqi55");
Set<String> sets= redisTemplate.opsForSet().members("user:id");
sets.stream().forEach(key-> System.out.println(key));
2、排序统计
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议你优先考虑使用 Sorted Set
ZSetOperations<String, String> zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("Goods","nice" , System.currentTimeMillis());
zSetOperations.add("Goods","pretty", System.currentTimeMillis());
zSetOperations.add("Goods","bad", System.currentTimeMillis());
zSetOperations.add("Goods","middle", System.currentTimeMillis());
3、二值状态统计
现在,我们再来分析下第三个场景:二值状态统计。这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态,
在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。这个时候,我们就可以选择 Bitmap。这是 Redis 提供的扩展数据类型。我来给你解释一下它的实现原理。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。你可以把 Bitmap 看作是一个 bit 数组。
我们可以计算一下记录了 10 天签到情况后的内存开销。每天使用 1 个 1 亿位的 Bitmap,大约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力不算太大。不过,在实际应用时,最好对 Bitmap 设置过期时间,让 Redis 自动删除不再需要的签到记录,以节省内存开销。
所以,如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。在记录海量数据时,Bitmap 能够有效地节省内存空间。
redisTemplate.opsForValue().setBit("userId:001:07",22,true);
redisTemplate.opsForValue().setBit("userId:001:07",24,true);
boolean flag = redisTemplate.opsForValue().getBit("userId:001:07",24);
4、基数统计
我们再来看一个统计场景:基数统计。基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中,就是统计网页的 UV。
网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中,Set 类型默认支持去重,所以看到有去重需求时,我们可能第一时间就会想到用 Set 类型,但是它会消耗很大的内存空间。Hash 类型也会消耗很大的内存空间。
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。
在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
//redis命令
PFADD page1:uv user1 user2 user3 user4 user5
//去重统计
PFCOUNT page1:uv
HyperLogLogOperations hyperLogLogOperations = redisTemplate.opsForHyperLogLog();
for(int i=0 ;i<10000;i++){
hyperLogLogOperations.add("UV:20220803","uv"+i);
}
long count= hyperLogLogOperations.size("UV:20220803");
不过,有一点需要你注意一下,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。虽然误差率不算大,但是,如果你需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型。
差集计算来说,只有 Set 支持。
当需要进行排序统计时,List 中的元素虽然有序,但是一旦有新元素插入,原来的元素在 List 中的位置就会移动,那么,按位置读取的排序结果可能就不准确了。而 Sorted Set 本身是按照集合元素的权重排序,可以准确地按序获取结果,所以建议你优先使用它。
如果我们记录的数据只有 0 和 1 两个值的状态,Bitmap 会是一个很好的选择,这主要归功于 Bitmap 对于一个数据只用 1 个 bit 记录,可以节省内存。
对于基数统计来说,如果集合元素量达到亿级别而且不需要精确统计时,我建议你使用 HyperLogLog。