12 有一亿个 key 要统计,使用哪种集合?

  • 应用场景:在 Web 应用的业务场景中,我们经常需要保存这样一组信息,一个 key 中对一整个集合。(例如:手机APP保存单个用户一天的登录记录信息,即一个 用户 ID 对应一组移动设备 ID 及其其他登录信息;电商网站上的商品评论信息,一个商品对应着一系列的评论信息;应用网站上的网页访问信息,一个网页对应的一系列的访问点击量等等场景)。
  • Redis 中的集合数据类型就是可以保存一个 Key 对应一整个集合这样的存储结构,满足上面的存储需求,但是我们对这些数据除了存储以外还可能需要对这些数据进行一些统计计算的需求(例如:移动应用中统计每天新增的用户量及其存留用户量;电商网站中需要统计评论列表及其最新的评论;签到打卡中统计一个月内连续签到的用户数量等等需求),当我们面向的时用户量巨大,百万,千万的访问量时,我们就需要一个能够支持高效的统计大量数据的集合类型。

一、Redis 中四种集合的统计模式

(1)聚合统计

  • 聚合统计就是多个集合之间进行聚合的结果,计算多个集合的差集,交集或者是并集。这些就是聚合统计中的差集统计,交集统计和并集统计。
  • 这种集合的统计模式就是用于计算应用网站中统计每天新增的用户量,存留用户量,总访问的用户量等等需求。首先创建一个 Set 集合的 key-value 来存储所有的访问的用户的 ID,例如创建一个 key 位 “user:id” ,而 value 中存储的每一条访问过网站的用户 ID。之后在每天创建一个全新的 Set 集合数据,其中 key 设置为 “user:id+日期”,然后 value 就是当前访问网站的所有的用户 ID,这样一来就可以保存每天的所有访问网站的用户 ID 数据了,根据当前的日期加上 “user:id” 作为 key 从 Redis 中获取指定的 value 数据,其中保存的是当前访问的所有的用户 ID,然后将该 Set 集合与 “user:id” 为 key 的 value 保存的所有访问该网站的用户 ID 进行取差集就得到了当天新增用户的 ID 集合,取交集就得到了当天留存的用户 ID 的集合。
  • 但是对于数据量比较大的情况下,使用 Set 集合来进行这些聚合计算的成本是比较高的,很容易就会造成 Redis 被阻塞,因此可以将这些数据交由从库来进行计算或者是将数据读取到客户端,由客户端来完成聚合统计。

(2)排序统计

  • 当我们获取对应商品的最新的评论的时候就需要根据评论的发布时间来对评论集合进行排序,其中在 Redis 中含有的集合中可以进行有序排序的有 List 和 Sort Set 两种数据类型。List 则是根据元素的添加顺序进行排序的,而Sort Set 是根据存储的数据的权重进行排序的。这两种数据类型我们又该如何选择?
  • 当我们展示商品评论的时候只采用一页来进行展示就可以将最新的评论展示在前面,早的评论放在后面。
  • 当我们需要对商品的评论信息进行分页获取的时候,例如每页数据只有三条评论的时候,我们需要获取第三页的评论,就会调用下面的命令,如果此时又有了新的评论就会将新的评论存储到 List 集合中,此时再调用下面的方法获取到的评论信息就和上次获取的评论不一致,产生这样的原因是 List 在获取数据的时候需要借助到评论在 List 集合的下标,因此当插入新的评论的时候,原来所有的评论的下标都发生了向后移动一位的变化,之后想再获取原来的评论数据的时候就会发生数据的偏差。
LRANGE listKey 0 2;
  • 而 Redis 中的 Sort Set 数据类型在存储数据的时候会设置数据的权重,我们可以将新评论的数据的权重递增设置的更大些,并且 Sort Set 集合再获取数据的时候是根据数据的权重来获取数据的,因此只要我们指定要获取数据的权重的范围就不会发生获取的数据前后不一致的错误。因此当我们需要存储的数据需要经常发生改动更新的情况下,我们就可以选择时候 Sort Set 来存储数据。

(3)二值状态统计

  • 在需要对所有的用户的签到记录进行存储的时候就需要为每个用户单独创建一个 key-value 进行存储,其中 key 用于保存用户 ID,value 用于保存用户的签到记录数据,其中可以将签到记录用 1 bit 来保存,签到为 1,未签到则保存为 0,这样记录一年的签到记录就仅仅只需要 356 bit 的长度,完全就不需要使用集合这样的复杂的数据类型。对于只有两种状态的取值,Reids 中就提供了扩展数据类型 Bitmap。
  • Bitmap 数据类型底层使用了 String 类型作为底层数据结构实现一种统计二值状态的数据类型,String 类型会保存为一个二进制的字节数组,这样就可以将字节数组中的每个 bit 用于保存二值数据,就可以将 bitmap 看作是一个 bit 数组。
  • Bitmap 中提供了 GETBIT / SETBIT 两种操作,并且使用 offset 偏移量对 bit 数组的某一个 bit 进行读和写,并且 offset 偏移量是从 0 开始的,因此 bit 数组中的数据也是从下标为 0 的位置开始写入的。当某个用户进行签到的时候,根据该用户的 ID 作为 key 到 Reids 全局哈希表中查找,找到 Bitmap 数据的时候,通过 offset 偏移量找到该 bit 数组写入的位置,并将该 bit 设置为 1,当用户没有写入的时候就不会进行写操作,因此该 bit 的值就为 0,而在 Bitmap 数据类型中,我们还可以调用 BITCOUNT 操作获取该字节数组中所有 “1” 的个数。
  • 当我们需要对某个用户(ID=8000)查看2022年8月的签到情况进行统计的时候就可以这样子做。创建一个键值对其中 key 设置为 “uid:sign:8000:202208”。
    • 当在 8 月 3 号进行签到的时候,就可以下面的命令,其中由于 offset 偏移量从 0 开始,因此该用户 8 月份的键值对中的 value 中 bit 字节数组的 offset 设置为 2,然后由于要进行签到,就将数值赋值为 1。
SETBIT uid:sign:8000:202208 2 1
setbit key offset value
  • 当要获取该 8000 用户在 2022 年 8 月 3 日的签到记录就调用下面的命令
GETBIT uid:sign:8000:202208 2
getbit key offset
  • 当我们需要统计 8000 用户在 2022 年 8 月的签到次数的数据时候就调用下面的命令
BITCOUNT uid:sign:8000:202208
bitcount key
  • 在上面的场景中是对所有的用户进行统计一个月的签到信息,那如何计算连续签到 10 天的用户量有哪些?首先要说明的是在 Bitmap 扩展数据类型中还可以进行 BITOP 操作(与、或、异或计算),因此当我们需要为一亿个用户统计连续签到 10 天进行统计,可以每天创建一个容量为一亿的 Bitmap 数据对象,根据每个用户的 ID 值,将 Bitmap 中的每一 bit 对应上一个用户 ID,然后将连续的十天内创建的所有的 Bitmap 数据对象进行与操作,并将计算的结果放入到 Bitmap 数据对象 resmap 中,之后,根据 resmap 中的记录,若数值为 1 则表示对应 ID 的用户在这连续的十天内坚持连续进行了签到。
  • 一亿个 bit 长度的 Bitmap 所占的内存虽然很小,但是在实际的应用中,应当给 Bitmap 设置一个过期事件,将一些不在需要的签到记录从 Redis 中删除,避免无效的占用内存。
    在这里插入图片描述

(4)基数统计

  • 当我们需要进行对某个页面统计访问量的时候,而且每一用户的多次访问只能记作一个有效的访问,因此就需要对相同用户 ID 的访问进行去重的操作,想到支持去重的数据结构,就会立马想到 Redis 中的 Set 和 Hash。当使用 Set 数据结构来存储进行去重存储数据的时候,例如电商网站,单个商品页面一天的的访问量可能就达到千万级别的,然也同时有着大量的商品页面,如果使用 Set 数据结构来存储单个页面的访问量,随着访问量的增加,需要占用的内存就越来越多。若使用的是 Hash 来进行存储每次当有用户访问该页面的时候就添加数据,并且每次 value 都只设置为 1,虽然也能达到去重的效果,但是弊端和 Set 一样,随着用户的访问量增加,其存储的数据所占的内存就越来越大。
  • 因此在 Redis 中对于基数统计的应用中,就使用了扩展类型 HyperLogLog 数据类型,该数据类型是用作统计基数的数据集合类型,相对于 Set 和 Hash 进行基数统计的最大优势就是集合元素非常多的时候,其所占的内存是固定的,并且非常小,在 Redis 中,一个 HyperLogLog 只需要占用 12KB 内存。当添加数据的时候使用PFADD 命令,当进行基数统计的时候就调用 PFCOUNT 命令,但是使用 HyperLogLog 有个弊端,就是统计的结果使用的是概率统计的方式,其标准误差率 0.81%。如果有精确统计的需求,那么就只能使用 Set 或者是 Hash 来存储访问数据。在这里插入图片描述

版权声明:本文为wmt0501原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。