Redis的集合类型的特点是一个键对应一系列值,非常适合用来存取统计的数据,比如:
每天的新增用户数和第二天的留存用户数;
统计评论列表的最新评论;
一个月内连续打卡的用户数;
UV量;
先了解一下常用的集合统计模式:
聚合统计,排序统计,二值状态统计,基数统计
聚合统计
聚合统计是指统计多个集合元素的聚合结果。包括:交集统计,差集统计,并集统计。
Set类型非常适合用来做聚合统计。
SINTER key [key ...] #返回指定所有的集合的成员的交集.时间复杂度O(N*M)
SUNION key [key ...] #返回给定的多个集合的并集中的所有成员.时间复杂度:O(N
SDIFF key [key ...] #返回一个集合与给定集合的差集的元素.O(N)
比如统计每天的新增用户数和第二天的留存用户数。
假设在2022年10月1号上线,10月1号前是没有数据的,累计用户set是空的。10月1号登陆的用户记录在key为user20221001的集合set中,计算累计用户set和user20221001的并集结果,存在user:id的集合中(user:id集合是累计用户集合),到了10月2号,登录的用户记录在user20221002的集合中,执行SDIFFSTORE计算user:id与user20221002的差集,存在user:new中,user:new就是10月2号的新增用户。对user20221001和user20221002进行交集计算就是留存用户数。
由于计算复杂度较高,需要选择一个从库,或者放在客户端去进行聚合统计,防止主库阻塞。
排序统计
需要对集合类型进行排序。
LIST和SOERTED SET是有序集合。
LIST是按照原始进入LIST的顺序进行排序
SORTED是按照元素的权重进行排序,我们可以自己来决定每个元素的权重值。
比如最新评论列表的场景。
当使用LIST存储时,每当有一个新评论,可以使用LPUSH插入到LIST的对头。
LPUSH product "A"```
LPUSH product "B"```
LPUSH product "C"```
LPUSH product "D"```
LPUSH product "E"```
LPUSH product "F"```
LPUSH product "G"```
时间复杂度是o(1),取数据的时候使用LRANGE
```powershell
//第一页
LRANGE product 0 2
1) "G"
2) "F"
3) "E"
//第二页
LRANGE product 3 5
1) "D"
2) "C"
3) "B"
但是分页的需求的时候,假设一页有三条,在展示第二页的时候,插入了一条G,最后的结果就会变成
127.0.0.1:6379> LPUSH product H
(integer) 8
127.0.0.1:6379> LRANGE product 3 5
1) "E"
2) "D"
3) "C"
可以看到在第一页的E会在第二页展示。
我们可以使用SORTED SET按照时间给每个评论设置一个权重值,就算集合中的元素被频繁更新,也能使用ZRANGEBYSCORE命令准确获取到按序排列的数据。
在面对数据更新频繁或者需要分页显示,优先考虑Sorted set
二值状态统计
集合元素的状态只有0和1两种。
比如签到,只需要记录签到1和未签到0.
可以使用REDIS提供的BITMAP。BITMAP的底层数据结构是是string,把字节数组的每个big位利用起来,表示一个元素的二值状态。
BITMAP提供
SETBIT key offset value #设置或者清空key的value(字符串)在offset处的bit值。时间复杂度:O(1)
GETBIT key offset #返回key对应的string在offset处的bit值 当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。当key不存在的时候,它就认为是一个空字符串,所以offset总是超出范围,然后value也被认为是由0比特填充的连续空间。到内存分配。时间复杂度:O(1)
BITCOUNT key [start end] #统计字符串被设置为1的bit数.时间复杂度:O(N)
假设需要统计id 3000的用户在2020年8月份的统计情况,可以:
#记录8月3号签到
SETBIT uid:sign:3000:202008 2 1
#检查8月3号是否签到
GETBIT uid:sign:3000:202008
#统计8月份签到次数
BITCOUNT uid:sign:3000:202008
如果需要统计1亿个用户连续10天签到的用户总数?
用每天的日期作为KEY,存储一亿用户的签到情况。我们对10天的bitmap做一次与的操作,结果是一个bitmap,然后使用BITCOUNT统计1的位数,就是签到的用户总数。内存情况,1一个一亿位大约占12MB的空间,10天大概占的就是120MAB。
基数统计
统计一个集合中不重复的元素个数。
比如统计一个网页的UV
REDIS的SET类型默认支持去重。
当有一个用户到来时:
SADD page1:uv user1
SADD key member [member ...] #添加一个或多个指定的member元素到集合的 key中.指定的一个或者多个元素member 如果已经在集合key中存在则忽略.如果集合key 不存在,则新建集合key,并添加member元素到集合key中.
如果key 的类型不是集合则返回错误.时间复杂度:O(N
如果PAGE1非常火爆,UV达到了千万,会消耗非常大的内存空间。
可以使用HASH类型记录UV:
HSET page2:uv user1 1
HSET key field value #设置 key 指定的哈希集中指定字段的值。
#如果 key 指定的哈希集不存在,会创建一个新的哈希集并与 key 关联。
#如果字段在哈希集中存在,它将被重写。时间复杂度:O(1)
同样HASHSET也避免不了消耗很大内存空间的问题。
HyperLogLog是一种统计基数的数据集合类型,优势在于当集合元素数量非常多的时候,计算基数的空间也是固定的,很小。每个HyperLogLog只需要花费12KB内存,就可以计算2^64个元素。
HyperLogLog是带有0.81%的误差的,如果需要精确统计值的时候,还是要使用set或者hash。
使用方法:
PFADD page3:uv user1 user2 user3 user4 user5
PFCOUNT page3:uv
(integer) 5
PFADD key element [element ...] #将除了第一个参数以外的参数存储到以第一个参数为变量名的HyperLogLog结构中.
#这个命令的一个副作用是它可能会更改这个HyperLogLog的内部来反映在每添加一个唯一的对象时估计的基数(集合的基
#数).
#如果一个HyperLogLog的估计的近似基数在执行命令过程中发了变化, PFADD 返回1,否则返回0,如果指定的key不存
#在,这个命令会自动创建一个空的HyperLogLog结构(指定长度和编码的字符串).
#如果在调用该命令时仅提供变量名而不指定元素也是可以的,如果这个变量名存在,则不会有任何操作,如果不存在,则#会创建一个数据结构(返回1)时间复杂度:O(1)
PFCOUNT key [key ...] #当参数为一个key时,返回存储在HyperLogLog结构体的该变量的近似基数,如果该变量不存在,则#返回0.
#当参数为多个key时,返回这些HyperLogLog并集的近似基数,这个值是将所给定的所有key的HyperLoglog结构合并到一#个临时的HyperLogLog结构中计算而得到的.
#HyperLogLog可以使用固定且很少的内存(每个HyperLogLog结构需要12K字节再加上key本身的几个字节)来存储集合
#的唯一元素.
#返回的可见集合基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值.
.```