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


    写在前面

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

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

    主要分为如下四种:

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

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

    2:聚合统计

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

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

    网站上线后第一天比如说是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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后某一时刻,比如第二天凌晨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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    就保存了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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时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)
    
    • 1
    • 2
    • 3
    • 4

    可以看到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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    然后某一时刻,比如第二天凌晨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"
    
    • 1
    • 2
    • 3
    • 4
    • 5

    就保存了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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时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"
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到留存用户是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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如下统计签到次数:

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以该天的bitmap结果是1110 0000

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

    所以该天的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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以该天的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
    
    • 1
    • 2
    • 3
    • 4

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

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

    4:基数统计

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

    1:HyperLogLog
    2:BitMap
    
    • 1
    • 2

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

    假定每天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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    统计UV:

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

    这样就获取了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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

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

    写在后面

    参考文章列表:

  • 相关阅读:
    AC自动机
    最短路径(Dijkstra算法与Floyd算法)
    力扣每日一题41:缺失的第一个正数
    Android开发之百度地图定位
    mysql—视图
    python抽取pdf中的参考文献
    自媒体二号大前端主题模板 WordPress主题
    【老王读Spring Transaction-1】从EnableTransactionManagement顺藤摸瓜,研究@Transactional的实现原理
    (57、58)性能分析命令2
    测试同学怎么参与codereview
  • 原文地址:https://blog.csdn.net/wang0907/article/details/127845489