Code前端首页关于Code前端联系我们

Redis实战篇:巧妙利用数据类型,实现亿级数据统计

terry 2年前 (2023-09-28) 阅读数 57 #未命名

在移动应用的业务场景中,我们需要存储这样的信息:key与数据采集相关,同时,数据中的数据集合必须经过统计排序。

常见场景如下:

  • 提供userId来判断用户的登录状态;
  • 近7天内2亿用户登录情况,统计7天内连续登录的用户总数;
  • 统计每天的新增用户数以及第二天的留存用户数;
  • 独立网站访问者(UV)统计
  • 最新评论列表
  • 按播放量划分的音乐列表

我们面对的用户通常是巨大的,有几百万、几千万甚至几千万甚至几千万数十亿的访问信息。

因此,我们需要选择一种可以非常高效地统计大量数据(例如数十亿)的集合类型。

如何选择合适的数据集,首先要了解常用的统计模型,并利用对数据的合理理解来解决实际问题。

四种统计类型:

  1. 二进制状态统计;
  2. 聚合统计;
  3. 订单统计;
  4. 基本统计数据。

本文将使用除 String、Set、Zset、List 和 Hash 之外的扩展数据类型 BitmapHyperLogLog

文中指令均可运行并通过在线调试Redis客户端,地址:https://try.redis.io/,超级方便。

举报

分享更多,付出更多。在不考虑回报的情况下,尽早为他人创造更多价值。从长远来看,这些努力将获得成倍的回报。

尤其是当你开始与他人合作时,不要害怕短期回报。这没有多大意义。更多的是练习你自己的愿景、观点和解决问题的能力。

二进制状态统计

马哥,什么是二进制状态统计?

这意味着集合中元素的值只有0和1。在登录登录场景以及用户是否登录时,只需要记录check -logged in ( 1) 未登录 (0 ) 未注册 (1) 或 未注册 (1) 或 0

如果我们在判断用户是否登录的场景中使用Redis的String类型的实现( key -> userId, value -> 0表示离线,1 - 登录),如果存储100万个用户 如果将登录状态存储为字符串,则需要存储100万个字符串,这会占用太多内存。

对于二进制状态的场景,我们可以使用位图来实现。比如我们用一位来表示登录状态,一亿个用户只占用一亿位内存≈(100000000 / 8/ 1024/1024) 12 MB。

大概的空间占用计算公式是:($offset/8/1024/1024) MB

什么是位图?

基本数据结构 Bitmap 使用 String 类型的 SDS 数据结构来存储位数组。 Redis 使用每个字节数组的 8 位。每个位代表元素的二进制状态(不是 0 就是 1)。

位图可以看成是一个以位为单位的数组。每个数组单元只能存储0或1。数组的下位索引在Bitmap中称为偏移量。

直观的看,我们可以理解为buf数组的每个字节由一行表示,每行有8位,8个格子代表这个字节的8位,如下图所示:

Redis 实战篇:巧用数据类型实现亿级数据统计

8 位构成一个字节,因此位图显着节省了存储空间。 这就是Bitmap的优点。

判断用户登录状态

如何使用位图从大量用户中判断某个用户是否在线?

Bitmap 提供GETBIT 和 SETBIT 操作,通过偏移值的偏移量在位数组的偏移位置读写一位。需要注意的是,偏移量是从0开始的。

只需要一个key=login_status来存储用户登录状态数据。使用用户ID作为偏移量,在线设置为1,离线设置为0。使用 GETBIT 查明相关用户是否在线。 5 亿用户只需要 6 MB 空间。

命令 SETBIT

SETBIT <key> <offset> <value>

设置或清除移位时键值的位值(只能为 0 或 1)。命令

GETBIT

GETBIT <key> <offset>

获取键值偏移处的位值。当key不存在时,返回0。

假设我们要查出ID=10086的用户的登录状态:

第一步,执行以下命令,表示该用户已登录在。

SETBIT login_status 10086 1

第二步是检查用户是否登录。返回值1表示已登录。

GETBIT login_status 10086

第三步注销,将offset对应的值设置为0。

SETBIT login_status 10086 0

用户每月登录状态

签到统计中,每个用户每天的签到情况用1位表示,而每年签到只需要365位。一个月最多有 31 天,并且只需要 31 位。

例如,统计数字为89757的用户在2021年5月应该如何登录?

key 可以设计为 uid:sign:{userId}:{yyyyMM},该月每一天的值 - 1 可以用作偏移量(因为偏移量从 0 开始,所以 偏移量 = 日期 - 1

)。

第一步是执行以下命令,记录用户在2021年5月16日登录。

SETBIT uid:sign:89757:202105 15 1

第二步是判断号码89757的用户是否在2021年5月16日登录。♿ –第三步,统计该用户5月份的登录次数,使用命令BITCOUNT。该指令用于计算给定位字段中值为 1 的位数。

BITCOUNT uid:sign:89757:202105

这样我们就可以了解每个月的用户登录情况,不是很好吗?

本月第一次登机时间如何计算?

Redis 提供指令 BITPOS key bitValue [start] [end]。返回的数据表示位图中第一个值的偏移位置,即位值

默认情况下,该命令检测整个位图。用户可以使用可选参数start和参数end指定要检测的范围。

因此我们可以通过执行以下命令得到userID = 89757。 2021 年 5 月, 首次捕获 日期:

BITPOS uid:sign:89757:202105 1

请注意,我们需要返回 + 1 来指示第一个时钟天,因为偏移量从 0 开始。总计到

我们使用每日日期作为位图键,用户 ID 作为偏移量。如果是签到,则将偏移位置的位设置为1。

对应的集合中的每一位数据都是该日期的用户记录。

这样的位图一共有 7 个。如果我们可以对这7个位图的对应位进行AND运算。

相同的UserID转变是相同的。当用户ID在7个位图对应的偏移位置有bit=1时,表示该用户已连续连接7天。

结果保存在新的位图中。然后我们用BITCOUNT来统计位数=1,得到连续7天登录的用户总数。

Redis 提供BITOP 操作 destkey key [key ...]该命令用于对 key = key 的一个或多个位图进行按位运算。

操作可以是或❙❙❙

异或。当 BITOP 处理不同长度的字符串时,较短字符串的缺失部分被视为 0

空键也被视为包含0的字符串。

简单易懂,如下图所示:

Redis 实战篇:巧用数据类型实现亿级数据统计

3 个位图,将对应的位进行“与”运算,将结果存储到新的位图中。运算指令

表示对三个位图进行AND运算,并将结果存入destmap。然后对目标图进行BITCOUNT统计。

// 与操作
BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
// 统计 bit 位 =  1 的个数
BITCOUNT destmap

简单计算一亿个bit位图占用的内存开销大约需要12MB内存(10^8/8/1024/1024)。 7 天位图的内存开销约为 84 MB。 同时,我们应该给Bitmap设置一个过期时间,让Redis删除过期的数据,以节省内存

总结

想法是最重要的。当我们遇到只需要统计数据的二进制状态的统计场景时,比如用户是否存在、IP地址是否被列入黑名单、登录登录统计等,我们可以考虑使用它。位图。

只需要一位来表示0和1,这将大大减少计算大数据时的内存占用。

严重性统计

严重性统计:统计集合中唯一元素的数量,通常用于统计唯一用户(UV)的数量。

实现基数统计最直接的方法是使用集合等数据结构。如果该元素从未出现过,则将该元素添加到集合中;如果出现,则文件保持不变。

当页面访问量很大时,需要非常大的集合来统计,占用很大的空间。

另外,这样的数据可能不是很准确。有更好的解决方案吗?

这是个好问题。 Redis提供了数据结构HyperLogLog来解决各种场景下的统计问题。

HyperLogLog是一种不精确的基本重复数据删除方案。其统计规则基于概率,标准误差为0.81%。这个精度足以满足UV统计要求。

HyperLogLog 的原理太复杂了。了解更多请访问:

  • https://www.zhihu.com/questio...
  • https://en.wikipedia.org/wiki...

UV网站

是通过Set实现的

用户每天对某个网站的多次访问只能算一次,所以很容易想象使用Redis Set集合。

当用户号 89757 访问“为什么 Redis 这么快”时,我们把这个信息放到 Set 中。

SADD Redis为什么这么快:uv 89757

当用户号89757多次访问“为什么Redis这么快”页面时,Set的去重功能可以保证同一个用户ID不被重复记录。

使用命令SCARD在“为什么Redis这么快”页面上统计UV。该命令返回集合中元素的数量(即用户 ID)。

SCARD Redis为什么这么快:uv

代码

是通过哈希实现的。也可以使用Hash类型来实现,以用户ID作为哈希集合键。访问该页面时,执行HSET命令将该值设置为1。

即使用户多次访问并执行该命令,该userId的值也只会被设置为“1”。

最后使用命令HLEN统计哈希集中的元素数量,即UV。

如下:

HSET redis集群:uv userId:89757 1
// 统计 UV
HLEN redis集群

HyperLogLog王解决方案

代码又旧又湿。 Set虽然好,但是如果文章很受欢迎,达到千万级,Set会保存几千万用户的ID,消耗的内存会太多页。太大。同理,Hash数据类型也是如此。该怎么办?

使用Redis提供的HyperLogLog高级数据结构(不要只知道Redis的五种基本数据类型)。这是用于功率统计的数据收集类型。虽然数据量很大,但是计算基数所需的空间是固定的。

每个HyperLogLog 只需要花费最多 12 KB 的内存来计算以 2 为底的 64 个元素次方。

Redis 优化了HyperLogLog 的存储。当数量比较小时,存储空间采用系数矩阵,占用空间很小。

只有当数量非常大且稀疏矩阵占用的空间超过阈值时,才会转换为占用12KB空间的稠密矩阵。

PFADD

将访问该页面的每个用户 ID 添加到 HyperLogLog

PFADD Redis主从同步原理:uv userID1 userID 2 useID3

PFCOUNT

在“Redis主从同步原理”页面使用PFCOUNT获取UV值。

PFCOUNT Redis主从同步原理:uv

PFMERGE 使用场景

HyperLogLog 除了上述PFADDⓙ 和 ⓙ 和 ⓙ ⓙ 除了,我们还提供 PFMERGE 将多个 HyperLogLog 合并以创建新值 HyperLogLog

语法

PFMERGE destkey sourcekey [sourcekey ...]

使用场景

例如,我们在网络上有两个内容相似的页面。该行动表示,两个站点的数据需要合并。

还需要合并UV页面访问,那么PFMERGE就可以派上用场,所以这两个页面被同一个用户访问一次。

如下图:Redis和MySQL两个位图集合存储两页用户访问数据。

PFADD Redis数据 user1 user2 user3
PFADD MySQL数据 user1 user2 user4
PFMERGE 数据库 Redis数据 MySQL数据
PFCOUNT 数据库 // 返回值 = 4

将多个 HyperLogLog 合并为一个 HyperLogLog。合并后的 HyperLogLog 的基数接近所有输入 HyperLogLog 的观察集的统一集

user1和user2都访问过Redis和MySQL,且仅访问过一次。

排序统计

Redis集合的四种类型(列表、集合、散列、排序集)中,列表和排序集是有序的。

  • 列表:按照元素插入列表的顺序排序。使用场景通常可以用作消息队列、最近列表或评分列表;
  • 排序集:按元素权重排序。我们可以自己确定每个元素的权重值。 。使用场景(评级列表,例如按观看次数和点赞数)。

最新评论列表

老师妈妈,我可以使用列表插入顺序来实现评论列表

比如微信后台回复列表公众号(不用,举个例子),每个公众号对应一个存储所有用户评论公众号的列表。

每当用户发表评论时,请使用 LPPUSH 键值 [value ...] 插入到列表标题中。

LPUSH 码哥字节 1 2 3 4 5 6

然后使用LRANGE键星停止获取列表指定范围内的元素。

> LRANGE 码哥字节 0 4
1) "6"
2) "5"
3) "4"
4) "3"
5) "2"

注意,并不是所有最近的列表都可以使用列表来实现,因为对于频繁更新的列表,列表类型的分页可能会导致列表元素重复或丢失。

例如当前评论列表List ={A,B,C,D},左侧为最新评论,D为最早评论。

LPUSH 码哥字节 D C B A

显示第一页最后2条评论,得到A和B:

LRANGE 码哥字节 0 1
1) "A"
2) "B"

按照我们想要的逻辑,第二页可以通过 LRANGE Code Byte 2 3

PFMERGE destkey sourcekey [sourcekey ...]

如果在显示第二页之前会生成一个新的注释 E,注释 E 通过 LPUSH 代码字节 E 插入到 List 的头部,List = {E, A, B, C, D} 。

立即执行LRANGE code byte 2 3获取第二页注释,发现又显示了B。

LRANGE 码哥字节 2 3
1) "B"
2) "C"

出现这种情况的原因是List是按元素位置排序的。插入新元素后List = {E, A, B, C, D}

原始数据在列表中后移一位,导致所有旧元素被读取。

Redis 实战篇:巧用数据类型实现亿级数据统计

总结

只有不需要分页的列表(例如总是只取列表的前5个元素)或更新频率较低的列表(例如每天早上更新一次统计数据)才适合使用List来实现类型。

对于需要频繁分页和更新的列表,必须使用Sorted Set类型来实现。

另外,列表类型无法实现在某个时间范围内查找最后一个列表。必须通过Sorted Set类型来实现,比如以交易时间范围作为查询条件的订单列表。

评测

马老师,对于最新的list场景,list和sorted set都可以实现。为什么使用列表?直接使用Sorted Set不是更好吗?还可以设置按分数权重排序,更加灵活。

原因是Sorted set类型占用的内存容量比List类型占用的内存容量大几倍。在列表数量不大的情况下,可以使用Sorted Set类型。

比如每周的音乐列表,我们需要实时更新播放音量并显示在页面上。

另外,排序是根据播放音量来决定的,暂时无法满足列表。

我们可以将音乐ID存储在Sorted Set集合中,score将被设置为每首歌曲的播放音量,并且每次播放音乐时都会设置score = Score +1。

ZADD

进入音乐,例如我们添加《青花瓷》和《花田错》播放热门收藏:❙ZINCRBY‶❙ZINCRBY‶播放指令ZINCRBY

获得 + 1。 ?得到它?

ZREVRANGE

您可以使用命令ZREVRANGE 开始停止[WITHSCORES]

元素按 分数 按降序(从大到小)排序。

具有相同分数的成员按字典顺序逆序排列。

> ZREVRANGE musicTop 0 0 WITHSCORES
1) "青花瓷"
2) 100000000

总结

即使集合中的元素经常更新,排序集合也可以使用命令ZRANGEBYSCORE 准确检索排序后的数据。

对于需要展示最新列表、评分等场景,如果数据更新频繁或者需要在页面上展示,建议优先使用排序集。

聚合统计

是指计算一个集合的几个元素聚合的结果,例如:

  • 计算几个元素的公共数据(交集);
  • 计算两个集合之一的唯一元素(差异集统计);
  • 统计多个集合的所有元素(分支统计)。

马老师,你会在什么场景下使用交集、差集、并集?

Set Redis类型支持集合内的增删改查。基础层使用哈希数据结构。加法和减法的时间复杂度都是 O(1)。

A 支持多个集合之间的交叉、连接和差异操作。利用这组运算来解决上面的统计问题。

交集 - 共同好友

例如QQ中的共同好友,就是汇总统计中的交集。我们使用帐户作为 Key,使用帐户的好友作为 Set value。

模拟两个用户的好友集合:

SADD user:码哥字节 R大 Linux大神 PHP之父
SADD user:大佬 Linux大神 Python大神 C++菜鸡

Redis 实战篇:巧用数据类型实现亿级数据统计

只需要两个集合的交集即可将两个用户的共同好友求和,如下:

SINTERSTORE user:共同好友 user:码哥字节 user:大佬

执行命令“user:codebyte”后,交集数据两组中的“用户:老板”存储在集合中 user: 普通好友 -0 例如 2021-06-01 注册用户总数存储在 Key = User: 20210601 SET集合,2021-06-02 2021-06-202 的用户总数存储在 Key = User: 在集合 20210602.

Redis 实战篇:巧用数据类型实现亿级数据统计

以下命令将计算差异集并存储结果集合为user:new

SDIFFSTORE  user:new  user:20210602 user:20210601

执行完成后,user:new集合将代表2021/06/02新增用户数。

另外,QQ上有一个你可能认识的人的功能,也可以使用差分集来实现。这意味着从你朋友的朋友圈中减去你们共同的朋友就是你可能认识的人。

Unia - 全新的朋友

仍然是差异的一个例子。要统计2021/06/01和2021/06/02这两天的新用户总数,只需将这两个集合连接起来即可。

SUNIONSTORE  userid:new user:20210602 user:20210601

目前有新用户id收藏:近两天新添加的好友。

总结

集合的差、并、交的计算具有很高的计算复杂度。如果数据量很大,直接进行这些计算,Redis实例就会被阻塞。

因此,你可以部署一个专门用于统计的集群,让它负责聚合计算,或者读取数据到客户端,在客户端填充聚合统计数据,避免其他服务响应阻塞失败。 。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门