redis之常见的集合操作有哪些?

写在前面

在这里插入图片描述
redis对集合提供了相关的集合操作,比如取差集,并集,等,但是单纯的来看这些操作还是比较枯燥的,所以我们来借助具体的业务场景来学习下。接下来,我们就开始吧!

1:集合类型常见的统计模式

主要分为如下四种:

聚合统计:即统计多个元素的聚合结果,比如交集,并集,差集等。
二值状态统计:值只有是和否两种情况的统计,比如打卡和未打卡,签到和未签到,同意和不同意等。
基数统计:去重求和,比如UV等。

接下里我们通过不同的统计模式来学习各种不同的集合操作。

2:聚合统计

假定这样的场景,需要统计网站每日的新增用户数以及第二天的用户留存数(某日新增用户在第二天也登陆了,则为第二天的留存),我们定义使用Set来存储如下的信息来完成需求:

key:"user:id",存储所有登录过的用户信息
key:"user:id:YYYYMMdd",存储某天登陆过的用户信息
key:"user:new:YYYYMMdd",存储某天的新增用户
key:"user:retain:YYYYMMdd":存储某天的留存用户,即前一天新增,当天登录的用户

网站上线后第一天比如说是20221030,当天登录用户ID为1,2,3,如下操作模拟当日登录用户如下:

127.0.0.1:6379> sadd user:id:20221030 1
(integer) 1     
127.0.0.1:6379> sadd user:id:20221030 2
(integer) 1     
127.0.0.1:6379> sadd user:id:20221030 3
(integer) 1     
127.0.0.1:6379> smembers user:id:20221030
1) "1"
2) "2"
3) "3"

然后某一时刻,比如第二天凌晨2点计算昨天的新增用户,在user:id:20221030集合中的用户就是20221030的所有登录用户,计算如下:

127.0.0.1:6379> sdiffstore user:new:20221030 user:id:20221030 user:id
(integer) 3
127.0.0.1:6379> smembers user:new:20221030
1) "1"
2) "2"
3) "3"

就保存了20221030的新增用户到user:new:20221030,接下来我们还需要将登录用户合并到所有用户集合中,即进行并集操作,如下:

127.0.0.1:6379> sunionstore user:id user:id user:id:20221030
(integer) 3
127.0.0.1:6379> smembers user:id
1) "1"
2) "2"
3) "3"

此时user:id集合中就保存了所有的登录用户信息,然后我们来计算20221030的用户留存,在20221029日新增并且在20221030有登录的用户即为30日的留存用户,即取交集(当然29肯定是没有数据的,但是不影响我们的计算结果,并且这种方式我们的程序也不需要对这种边界情况做特殊处理,而是对其进行兼容),如下:

127.0.0.1:6379> sinterstore user:retain:20221030 user:id:20221030 user:new:20221029
(integer) 0
127.0.0.1:6379> smembers user:retain:20221030
(empty list or set)

可以看到20221030并没有留存用户。接着时间来到20221031,假设当天登录的用户信息为1,3,5,6,如下操作模拟当日登录用户:

127.0.0.1:6379> sadd user:id:20221031 1
(integer) 1
127.0.0.1:6379> sadd user:id:20221031 3
(integer) 1
127.0.0.1:6379> sadd user:id:20221031 5
(integer) 1
127.0.0.1:6379> sadd user:id:20221031 6
(integer) 1
127.0.0.1:6379> smembers user:id:20221031
1) "1"
2) "3"
3) "5"
4) "6"

然后某一时刻,比如第二天凌晨2点计算昨天的新增用户,在当日登录ID集合user:id:20221031中有而在总用户ID集合user:id中没有的即为当日的新增用户,即取差集,计算如下:

127.0.0.1:6379> sdiffstore user:new:20221031 user:id:20221031 user:id
(integer) 2
127.0.0.1:6379> smembers user:new:20221031
1) "5"
2) "6"

就保存了20221031的新增用户到user:new:20221031,接下来我们还需要将登录用户合并到所有用户集合中,即进行并集操作,如下:

127.0.0.1:6379> sunionstore user:id user:id user:id:20221031
(integer) 5
127.0.0.1:6379> smembers user:id
1) "1"
2) "2"
3) "3"
4) "5"
5) "6"

此时user:id集合中就保存了所有的登录用户信息,然后我们来计算20221031的用户留存,在20221030日新增登录用户ID并且在20221031也登录的用户即为31日的留存用户,即取交集,如下:

127.0.0.1:6379> sinterstore user:retain:20221031 user:id:20221031 user:new:20221030
(integer) 2
127.0.0.1:6379> smembers user:retain:20221031
1) "1"
2) "3"

可以看到留存用户是13,后续就是重复这个过程就行了。

注意:计算交集,并集,差集等的过程时间复杂度很高,占用大量的CPU,可能会导致服务器实例的阻塞,所以在线上环境中建议在从库执行该类操作,或者是将数据查询出来在应用程序中执行,个人更加倾向于使用后者,毕竟让数据库做纯粹的数据存取工作就够了。

3:二值状态统计

二值状态指的是值只有1和0两种,比如打卡场景,1代表打卡了0代表没有打卡,在redis中提供了BitMap ,接下来我们通过不同的场景来看下。

3.1:统计用户的签到次数

设置key格式为userid:[具体用户ID]:YYYYMM,如下模拟签到:

127.0.0.1:6379> setbit userid:1001:202210 2 1
(integer) 0
127.0.0.1:6379> setbit userid:1001:202210 3 1
(integer) 0
127.0.0.1:6379> setbit userid:1001:202210 12 1
(integer) 0

如下统计签到次数:

127.0.0.1:6379> bitcount userid:1001:202210 0 -1
(integer) 3

3.2:统计完成连续签到的用户数

假设网站要搞一个活动,1亿用户连续签到10天,其中有一个需求项是活动结束后要统计完成签到10天的人数,这里假定用户ID是从0开始严格递增的,此时我们可以按照天来定义一个长度为一亿bit的BitMap,如果是对应的比特位是1,则代表对应的用户签到了,否则没有签到,最终我们定义十个这样的bitmap代表10天所有用户用户的签到信息,这里一亿个比特位大小大概是100000000/8/1024/1024=12.5M*10=125M,从内存的使用量上来说其实是可以接受的,那么如何统计完成连续签到的人数呢,我们需要用到bitop来对这10个bitmap执行AND运算,只有连续每天都签到用户对应的位置才都会为1,AND的结果也才会为1,之后我们对bitop获取的bitmap执行bitcount,获取其中为1的个数就是我们想要的结果了。这里我们模拟3个用户,连续签到3天,假设用户ID分别是0,1,2,演示如下:

  • 模拟第1天签到
    假设3个人都签到了:
127.0.0.1:6379> setbit sign:day:01 0 1
(integer) 0
127.0.0.1:6379> setbit sign:day:01 1 1
(integer) 0
127.0.0.1:6379> setbit sign:day:01 2 1
(integer) 0

所以该天的bitmap结果是1110 0000

  • 模拟第2天签到
    假设只有ID为1的用户签到了:
127.0.0.1:6379> setbit sign:day:02 1 1
(integer) 0

所以该天的bitmap结果是0100 0000

  • 模拟第3天签到
    假设3个人都签到了:
127.0.0.1:6379> setbit sign:day:03 0 1
(integer) 0
127.0.0.1:6379> setbit sign:day:03 1 1
(integer) 0
127.0.0.1:6379> setbit sign:day:03 2 1
(integer) 0

所以该天的bitmap结果是1110 0000

  • 计算完成连续签到人数
127.0.0.1:6379> bitop AND sign:all sign:day:01 sign:day:02 sign:day:03
(integer) 1
127.0.0.1:6379> bitcount sign:all 0 -1
(integer) 1

sign:all结果是0100 0000,所以完成签到的人数为1。

其他的二值状态场景,比如用户在不在线,方案赞成反对,面试通过不通过等,都可以使用Bitmap来实现,Bitmap使用非常少的内存就可以解决大数据量下的二值状态场景问题,因此是工作中的一把利器。

4:基数统计

基数统计就是统计一个集合中不重复的元素的个数,典型的场景比如统计每天的UV数。此时对于少量数据我们可以直接采用Set,Hash的集合类型来直接存储,因为其天生的具有去重的功能,但是当数据量到达一定级别之后,这些数据结构都会占用非常多的内存控件,比如每天会有1000万的用户点击,则就要存储1000万个元素,此时就需要考虑其他的设计方案了,可以考虑如下的两种方案:

1:HyperLogLog
2:BitMap

接下来我们分别看下这两种方案如何实现。

假定每天set存储1000万个元素,假定每个元素大小8byte,则每天大小就是10000000*8byte=80000000byte=80000Kb=80M,每天80M也是一个不小的内存消耗了。而hyperloglog的消耗在k级别,bitmap消耗在1M左右,相比于直接使用集合都要小得多。

4.1:HyperLogLog

HyperLogLog是一种基数估算的算法,在LogLog算法的基础上进行了增强,如下模拟用户点击的过程:

127.0.0.1:6379> pfadd somepage:uv:20221010 111 222 # 用户111 222点击了
(integer) 1
127.0.0.1:6379> pfadd somepage:uv:20221010 111 333 444 # 用户111 333 444点击了
(integer) 1
127.0.0.1:6379> pfadd somepage:uv:20221010 333 222 555 # 用户333 222 555点击了
(integer) 1

统计UV:

127.0.0.1:6379> pfcount somepage:uv:20221010
(integer) 5

这样就获取了UV值为5,注意这里虽然获取的是一个精确的值,这是因为我们数据量较小,当数据量比较大时,就会有一定的误差了,大概1000万会存在1到2万误差的样子,如果允许这种少量误差的话就可以考虑使用HyperLogLog,否则我们也可以考虑使用BitMap。

4.2:BitMap

采用BitMap的话,我们首先就可以将该问题转换为二值状态统计,即某用户点击或者是没有点击某页面,假定用户ID是连续的整数(因为要使用其作为BitMap的偏移量,当然我们也可以额外维护这样的一个userid->整数的对应关系),如下模拟用户12345的点击过程:

127.0.0.1:6379> setbit www.baidu.com:uv:20221010 1 1 # 用户1点击了
(integer) 0
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 2 1 # 用户2点击了
(integer) 0
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 2 1 # 用户2点击了
(integer) 1
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 3 1 # 用户3点击了
(integer) 0
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 5 1 # 用户5点击了
(integer) 0
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 1 1 # 用户1点击了
(integer) 1
127.0.0.1:6379> setbit www.baidu.com:uv:20221010 4 1 # 用户4点击了
(integer) 0

接下来统计UV,即位为1的个数:

127.0.0.1:6379> bitcount www.baidu.com:uv:20221010
(integer) 5

这样不管数据量多大,获取的肯定是准确的数字,并且一般不会有内存问题。虽然计算准确但是对内存的消耗相比于HyperLogLog还是更多的,12KB的内存HyperLogLog最大可以表示2^64次方个基数,而BitMap只能表示12*1000*8=96000,即96000个基数。如果是页面点击的场景,就是计算最多96000个用户的UV。但是这个内存上的消耗差距,对于现代计算机几乎可以忽略,因为12M的数据,BitMap就能存储将近一亿个用户的点击信息了,而12M数据显然不算是特别大的内存消耗,所以如果是在工作遇到这种基数统计场景的话,建议选择使用BitMap方案,因为相比于hyperloglog统计更加准确,且内存消耗可控。

写在后面

参考文章列表:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值