集合统计模式
聚合统计
所谓的聚合统计,就是指统计多个集合元素的聚合结果,包括:统计多个集合的共有元素 (交集统计);把两个集合相比,统计其中一个集合独有的元素(差集统计);统计多个 集合的所有元素(并集统计)
场景,统计手机 App 每天的新增用户数和第二天的留存用户数。
要完成这个统计任务,我们可以用一个集合记录所有登录过 App 的用户 ID,同时,用另 一个集合记录每一天登录过 App 的用户 ID。然后,再对这两个集合做聚合统计。
具体操作
记录所有登录过 App 的用户 ID 还是比较简单的,我们可以直接使用 Set 类型,把 key 设 置为 user:id,表示记录的是用户 ID,value 就是一个 Set 集合,里面是所有登录过 App 的用户 ID,我们可以把这个 Set 叫作累计用户 Set。
把每一天登录的用户 ID,记录到一个新集合中,我们把这个集合叫作每 日用户 Set,它有两个特点: 1. key 是 user:id 以及当天日期,例如 user:id:20220114; 2. value 是 Set 集合,记录当天登录的用户 ID。
差集计算
1 | SDIFFSTORE user:new user:id:20220114 user:id |
把在当日登录set中的,而不在累计用户中的,记到user:new中,就是新增用户
做并集
1 | SUNIONSTORE user:id user:id user:id:20220114 |
每天将前一天的当日登录set和累计用户 Set进行并集运算,存到累计用户set中
交集运算
求留存用户,指的是20220113登录了,20220114又登录了,就是20220114的留存用户据
1 | SINTERSTORE user:id:rem user:id:20220113 user:id:20220114 |
风险
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计 算,会导致 Redis 实例阻塞。
所以,我给你分享一个小建议:你可以从主从集群中选择一 个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统 计,这样就可以规避阻塞主库实例和其他从库实例的风险了。
排序统计
List 和 Sorted Set 就属于有序集合。
List 是按照元素进入 List 的顺序进行排序的,而 Sorted Set 可以根据元素的权重来排 序,我们可以自己来决定每个元素的权重值。比如说,我们可以根据元素插入 Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。
List的操作
LPUSH 命令把它插入 List 的队头。
LRANGE 命令获取部分元素。通过index位置来取。
Sorted Set
ZRANGEBYSCORE 命令就可以按权重排序后返回元素
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显 示,建议你优先考虑使用 Sorted Set。
二值状态统计
二值状态就是指集合元素的取 值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所 以它就是非常典型的二值状态
在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签 到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂 的集合类型。这个时候,我们就可以选择 Bitmap。这是 Redis 提供的扩展数据类型。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。你可以把 Bitmap 看作是一个 bit 数组
Bitmap 提供了 GETBIT/SETBIT 操作
Bitmap 还提供了 BITCOUNT 操作,用来统计这个 bit 数组中所有“1”的个数。
Bitmap 支持用 BITOP 命令对多个 Bitmap 按位 做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。
场景:统计 1 亿个用户连续 10 天的签到情况
可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。
1 个 1 亿位的 Bitmap,大约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约 为 120MB,内存压力不算太大。
对 10 个 Bitmap 做“与”操作,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。
基数统计
基数统计就是指统计一个集合中不重复的元 素个数。对应到场景中,就是统计网页的 UV。
这个问题的问题点就是在去重,在 Redis 的集合类型中,Set 类型默认支持去重,所以看到有去重需求时,我们可能 第一时间就会想到用 Set 类型。
SADD page1:uv user1
SCARD 命令,返回一个集合中的元素个数。
也可以用hash,利用key不会重复的特性。
但是上面这两个方案,都消耗很大的内存空间
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数 量非常多时,它计算基数所需的空间总是固定的,而且还很小。
在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素 的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非 常节省空间。
PFADD
PFCOUNT
不过,有一点需要你注意一下,HyperLogLog 的统计规则是基于概率完成的,所以它给出 的统计结果是有一定误差的,标准误算率是 0.81%。
