Redis核心技术09-keys统计案例与方案
# Redis核心技术09-keys统计案例与方案
一个key对应了一个数据集合的例子:
- 手机 App 中的每天的用户登录信息:一天对应一系列用户 ID 或移动设备 ID;
- 电商网站上商品的用户评论列表:一个商品对应了一系列的评论;
- 用户在手机 App 上的签到打卡信息:一天对应一系列用户的签到记录;
- 应用网站上的网页访问信息:一个网页对应一系列的访问点击。
对上述案例的统计业务需求:
- 在移动应用中,需要统计每天的新增用户数和第二天的留存用户数;
- 在电商网站的商品评论中,需要统计评论列表中的最新评论;
- 在签到打卡中,需要统计一个月内连续打卡的用户数;
- 在网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。
下面我们重点介绍常见的四种统计模式,包括聚合统计、排序统计、二值状态统计和基数统计
# 聚合统计
聚合统计:就是指统计多个集合元素的聚合结果,包括:统计多个集合的共有元素(交集统计);把两个集合相比,统计其中一个集合独有的元素(差集统计);统计多个集合的所有元素(并集统计)。
统计手机 App 每天的新增用户数和第二天的留存用户数,正好对应了聚合统计。
我们可以用一个集合记录所有登录过App 的用户 ID,同时,用另一个集合记录每一天登录过 App 的用户 ID。然后,再对这两个集合做聚合统计。
所有登录过App的用户
我们可以直接使用 Set 类型,把 key 设置为 user:id,表示记录的是用户 ID,value 就是一个 Set 集合,里面是所有登录过 App 的用户 ID,我们可以把这个 Set 叫作累计用户 Set。
每日登录App的用户
我们还需要把每一天登录的用户 ID,记录到一个新集合中,我们把这个集合叫作每日用户 Set,它有两个特点:
- key 是 user:id 以及当天日期,例如 user : id:20200803;
- value 是 Set 集合,记录当天登录的用户 ID。
统计每天的新增用户时,我们只用计算每日用户set和累计用户set的差集就行了。
统计第二天的留存用户时,我们只用计算前一天用户set和当天用户set的交集就可以了。
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。
# 排序统计
电脑网站上提供最新评论列表的场景就需要使用到对集合元素排序的方法。在 Redis 常用的 4 个集合类型中(List、Hash、Set、Sorted Set),List 和 Sorted Set 就属于有序集合。
- List 是按照元素进入 List 的顺序进行排序的。
- Sorted Set 可以根据元素的权重来排序。
对于List,每个商品对应一个 List,这个 List 包含了对这个商品的所有评论,而且会按照评论时间保存这些评论,每来一个新评论,就用 LPUSH 命令把它插入 List 的队头。但是List一旦设计到分页就会存在问题。
比如现在有6个评论,{A, B, C, D, E, F},第一页展示A B C,如果在展示第二页之前,又产生了一个新评论 G,那么,评论 List 就变成了{G, A, B, C, D, E, F}。此时再用刚才的命令获取第二页评论时,就会发现,评论 C 又被展示出来了,也就是 C、D、E。之所以会这样,关键原因就在于,List 是通过元素在 List 中的位置来排序的,当有一个新元素插入时,原先的元素在 List 中的位置都后移了一位。
Sorted Set 就不存在这个问题,因为它是根据元素的实际权重来排序和获取数据的。我们可以按评论时间的先后给每条评论设置一个权重值,然后再把评论保存到 Sorted Set 中。Sorted Set 的 ZRANGEBYSCORE 命令就可以按权重排序后返回元素
假设越新的评论权重越大,目前最新评论的权重是 N,我们执行下面的命令时,就可以获得最新的 10 条评论:
ZRANGEBYSCORE comments N-9 N
# 二值状态统计
这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。
对于签到这种场景我们可以使用Bitmap。这是 Redis 提供的扩展数据类型。
Bitmap 提供了 GETBIT/SETBIT 操作,使用一个偏移值 offset 对 bit 数组的某一个 bit 位进行读和写。不过,需要注意的是,Bitmap 的偏移量是从 0 开始算的,也就是说 offset 的最小值是 0。当使用 SETBIT 对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。Bitmap 还提供了 BITCOUNT 操作,用来统计这个 bit 数组中所有“1”的个数。
记录该用户8月3号签到:
SETBIT uid:sign:3000:202008 2 1
检查用户8月3号是否签到:
GETBIT uid:sign:3000:202008 2
统计用户在8月份的签到次数:
BITCOUNT uid:sign:3000:202008
Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。
如果我们要统计1 亿个用户连续 10 天的签到情况时,可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。
接下来,我们对 10 个 Bitmap 做“与”操作,得到的结果也是一个 Bitmap。在这个 Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。
# 基数统计
基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中,就是统计网页的 UV。
网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中,Set 类型默认支持去重,所以看到有去重需求时,我们可能第一时间就会想到用 Set 类型。但是如果一个页面比较火爆,那么UV 达到了千万,这个时候,一个 Set 就要记录千万个用户 ID,这样就会非常消耗内存。
现在我们需要既能完成去重统计,还要节省内存,就可以使用HyperLogLog。
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。
不过,有一点需要注意一下,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。