Redis数据实战之统计一亿个keys,应该用哪种集合?

Redis数据实战之统计一亿个keys,应该用哪种集合?

引言

在 Web 和移动应用的业务场景中,我们经常需要保存这样一种信息:一个 key 对应了一个数据集合。我举几个例子。

1.手机 App 中的每天的用户登录信息:一天对应一系列用户 ID 或移动设备 ID;
2. 电商网站上商品的用户评论列表:一个商品对应了一系列的评论;
3.用户在手机 App 上的签到打卡信息:一天对应一系列用户的签到记录;
4.应用网站上的网页访问信息:一个网页对应一系列的访问点击。

Redis 集合类型的特点就是一个键对应一系列的数据,所以非常适合用来存取这些数据。但是,在这些场景中,除了记录信息,我们往往还需要对集合中的数据进行统计:

1.在移动应用中,需要统计每天的新增用户数和第二天的留存用户数;
2在电商网站的商品评论中,需要统计评论列表中的最新评论;
3.在签到打卡中,需要统计一个月内连续打卡的用户数;
4.在网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。

要想选择合适的集合,我们就得了解常用的集合统计模式。针对四种场景的四个问题,本篇博客将分别介绍集合的四种统计模式,包括聚合统计、排序统计、二值状态统计和基数统计

聚合统计

所谓的聚合统计,就是指统计多个集合元素的聚合结果,包括:

  • 统计多个集合的共有元素(交集统计);
  • 把两个集合相比,统计其中一个集合独有的元素(差集统计);
  • 统计多个集合的所有元素(并集统计)。

统计手机 App 每天的新增用户数和第二天的留存用户数,正好对应了聚合统计。要完成这个统计任务,我们可以用一个集合记录所有登录过 App 的用户 ID,同时,用另一个集合记录每一天登录过 App 的用户 ID。然后,再对这两个集合做聚合统计。

记录所有登录过 App 的用户 ID 还是比较简单的,我们可以直接使用 Set 类型,把 key 设置为 user:id,表示记录的是用户 ID,value 就是一个 Set 集合,里面是所有登录过 App的用户 ID,我们可以把这个 Set 叫作累计用户 Set。
在这里插入图片描述

累计用户 Set 中没有日期信息,我们是不能直接统计每天的新增用户的。所以,我们还需要把每一天登录的用户 ID,记录到一个新集合中,我们把这个集合叫作每日用户 Set,它有两个特点:

  1. key 是 user:id 以及当天日期,例如 user: id:20200803;
  2. value 是 Set 集合,记录当天登录的用户 ID。
    在这里插入图片描述

在统计每天的新增用户时,我们只用计算每日用户 Set 和累计用户 Set 的差集就行

假设我们的手机 App 在 2020 年 8 月 3 日上线,那么,8 月 3 日前是没有用户的。此时,累计用户 Set 是空集,当天登录的用户 ID 会被记录到 key 为 user: id:20200803 的Set 中。

我们计算累计用户 Set 和 user: id:20200803 Set 的并集结果,结果保存在 user:id这个累计用户 Set 中。此时,user:id 这个累计用户 Set 中就有了 8 月 3 日的用户 ID。

SUNIONSTORE user:id user:id user:id:20200803

等到 8 月 4 日再统计时,我们把 8 月 4 日登录的用户 ID 记录到 user: id:20200804 的 Set 中。
SDIFFSTORE 命令计算累计用户 Set 和 user: id:20200804 Set 的差集,结果保存在 key为 user:new 的 Set 中,user:new 这个 Set 中记录的就是 8 月 4 日的新增用户。

SDIFFSTORE user:new user:id:20200804 user:id

当要计算 8 月 4 日的留存用户时,我们只需要再计算 user: id:20200803 和user: id:20200804 两个 Set 的交集,就可以得到同时在这两个集合中的用户 ID 了。

SINTERSTORE user:id:rem user:id:20200803 user:id:20200804

Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。所以,我建议你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了

排序统计

最新评论列表包含了所有评论中的最新留言,这就要求集合类型能对元素保序,也就是说,集合中的元素可以按序排列,这种对元素保序的集合类型叫作有序集合。List 和 Sorted Set就属于有序集合。

List 是按照元素进入 List 的顺序进行排序的,而 Sorted Set 可以根据元素的权重来排序

  • 用 List 的情况。每个商品对应一个 List,这个 List 包含了对这个商品的所有评论,而且会按照评论时间保存这些评论,每来一个新评论,就用 LPUSH 命令把它插入 List的队头。但是,在实际应用中,网站一般会分页显示最新的评论列表,一旦涉及到分页操作,List 就可能会出现问题了
    关键原因就在于,List 是通过元素在 List 中的位置来排序的,当有一个新元素插入时,原先的元素在 List 中的位置都后移了一位,比如说原来在第 1 位的元素现在排在了第 2 位。所以,对比新元素插入前后,List 相同位置上的元素就会发生变化,用LRANGE 读取时,就会读到旧元素

  • Sorted Set 就不存在这个问题,因为它是根据元素的实际权重来排序和获取数据的。可以按评论时间的先后给每条评论设置一个权重值,然后再把评论保存到 Sorted Set中。Sorted Set 的 ZRANGEBYSCORE 命令就可以按权重排序后返回元素。这样的话,即使集合中的元素频繁更新,Sorted Set 也能通过 ZRANGEBYSCORE 命令准确地获取到按序排列的数据

在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议你优先考虑使用 Sorted Set

二值状态统计

二值状态统计。这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。在签到打卡的场景中,我们只用记录签到(1)或未签到(0)。在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。你可以把 Bitmap 看作是一个 bit 数组

  • Bitmap 提供了 GETBIT/SETBIT 操作,使用一个偏移值 offset 对 bit 数组的某一个 bit 位进行读和写。不过,需要注意的是,Bitmap 的偏移量是从 0 开始算的,也就是说 offset的最小值是 0。当使用 SETBIT 对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。
  • Bitmap 还提供了 BITCOUNT 操作,用来统计这个 bit 数组中所有“1”的个数

假设我们要统计 ID 3000 的用户在 2020 年 8 月份的签到情况,就可以按照下面的步骤进行操作。

第一步,执行下面的命令,记录该用户 83 号已签到:
SETBIT uid:sign:3000:202008 2 1

第二步,检查该用户 83 日是否签到:
GETBIT uid:sign:3000:202008 2

第三步,统计该用户在 8 月份的签到次数:
BITCOUNT uid:sign:3000:202008

如果记录了 1 亿个用户 10 天的签到情况,你有办法统计出这 10 天连续签到的用户总数吗?

Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中
在这里插入图片描述
在统计 1 亿个用户连续 10 天的签到情况时,你可以把每天的日期作为key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。我们对 10 个 Bitmap 做“与”操作,得到的结果也是一个 Bitmap。在这个Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。

不过,在实际应用时,最好对 Bitmap 设置过期时间,让Redis 自动删除不再需要的签到记录,以节省内存开销

基数统计

基数统计。基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中,就是统计网页的 UV。

网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中,Set 类型默认支持去重,所以看到有去重需求时,我们可能第一时间就会想到用 Set 类型

当你需要统计 UV 时,可以直接用 SCARD 命令,这个命令会返回一个集合中的元素个数。但是,如果 page1 非常火爆,UV 达到了千万,这个时候,一个 Set 就要记录千万个用户ID。对于一个搞大促的电商网站而言,这样的页面可能有成千上万个,如果每个页面都用这样的一个 Set,就会消耗很大的内存空间。

SADD page1:uv user1

当然,你也可以用 Hash 类型记录 UV。例如,你可以把用户 ID 作为 Hash 集合的 key,当用户访问页面时,就用 HSET 命令(用于设置 Hash 集合元素的值),对这个用户 ID 记录一个值“1”,表示一个独立访客。

HSET page1:uv user1 1

但是,和 Set 类型相似,当页面很多时,Hash 类型也会消耗很大的内存空间。

HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小

在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。你看,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

在统计 UV 时,你可以用 PFADD 命令(用于向 HyperLogLog 中添加新元素)把访问页面的每个用户都添加到 HyperLogLog 中。

PFADD page1:uv user1 user2 user3 user4 user5

接下来,就可以用 PFCOUNT 命令直接获得 page1 的 UV 值了,这个命令的作用就是返回 HyperLogLog 的统计结果。

PFCOUNT page1:uv

HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。但是,如果你需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型。

在这里插入图片描述

觉得有用的客官可以点赞、关注下!感谢支持🙏谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值