千亿级数据

核心提示在移动应用的业务场景中,我们需要保存这样的信息:一个 key 关联了一个数据集合。常见的场景如下:给一个 userId ,判断用户登陆状态;显示用户某个月的签到次数和首次签到时间;两亿用户最近 7 天的签到情况,统计 7 天内连续签到的用户

在移动应用的业务场景中,我们需要保存一个键与一个数据集相关联的信息。

常见场景如下:

给一个userId来判断用户的登录状态;显示用户某月的签到次数和首次签到时间;最近7天2亿用户签到状态,统计7天内连续签到的用户总数;

通常我们面对的用户数量和访问量都是巨大的,比如几百万或者几千万的用户,或者几千万甚至几十亿的用户访问信息。

因此,我们必须选择一种能够非常高效地统计大量数据的集合类型。

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

四种统计类型:

二元状态统计;统计;排序;基数。

本文将以二进制状态统计类型作为系列实战文章的开头,用String、Set、Zset、List、hash之外的扩展数据类型位图来实现。

本文涉及的指令可以通过在线Redis客户端运行调试,地址是https://try.redis.io/,,说起来很方便。

通知

多分享,多付出,前期多为别人创造价值,不要回报。从长远来看,这些努力会给你很多倍的回报。

尤其是在和别人合作的初期,不要担心短期回报,没有太大意义,更多的是锻炼自己的视野、视角和解决问题的能力。

二元状态统计

兄弟,什么是二进制状态统计?

也就是说,集合中元素的值只有0和1。在签到打卡和用户是否登录的场景下,只需要记录签到与否,登录与否。

如果在判断用户是否登录的场景中使用Redis的字符串类型实现,如果存储100万用户的登录状态,如果以字符串的形式存储,需要存储100万个字符串,内存开销太大。

兄弟,为什么字符串类型的内存开销大?

除了记录实际数据之外,类型字符串还需要额外的内存来记录数据长度、空的用法等信息。

当保存的数据包含字符串时,使用简单的动态字符串结构保存字符串类型,如下图所示:

学生争取民主社会运动(Students for a Democratic Society)ˌ十二烷基磺酸钠(Sodium Dodecyl Sulfonate)

Len: 4个字节,表示buf的使用长度。Alloc:占用4个字节,表示buf实际分配的长度,通常> len。Buf:字节数组,保存实际数据。Redis会自动在数组末尾添加一个“[22]”,这会占用额外的字节开销。

所以在SDS中,除了buf保存实际数据,len和alloc都是额外的开销。

另外,RedisObject结构还有一个开销,因为Redis的数据类型很多,不同的数据类型都有相同的元数据要记录。

所以Redis会用一个RedisObject结构来统一记录这些元数据,指向实际的数据。

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

近似空占用率公式为MB。

什么是位图?

位图的底层数据结构使用字符串SDS数据结构来存储位数组。Redis使用每个字节数组的8位,每一位代表一个元素的二进制状态。

位图可以看作是一个位的数组,数组的每个单元只能存储0或1,数组的下标在位图中称为offset。

为了直观显示,我们可以理解为buf数组的每个字节用一行来表示,每行8位,8个网格分别表示该字节中的8位,如下图所示:

位图

八位组成一个字节,所以位图会大大节省存储空。这就是位图的优势。

判断用户登录状态。

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

Bitmap提供GETBIT和SETBIT操作,通过一个偏移值在比特数组的偏移位置读写比特。应该注意,偏移量从0开始。

存储用户登录状态集数据只需要一个key = login_status。如果用户ID用作偏移量,它将被设置为在线1和离线0。GETBIT来确定相应的用户是否在线。5000万用户只需要6 MB 空间。

SETBIT命令

设置位

或者将clear 空键的值的位值设置为offset。

GETBIT命令

GETBIT

获取偏移量处key的值的位值,如果key不存在则返回0。

如果我们想判断ID = 10086的用户的登录状态:

第一步是执行以下指令来指示用户已经登录。

SETBIT login_status 10086 1

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

GETBIT login_status 10086

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

SETBIT login_status 10086 0

每月用户的登录状态

在签到统计中,每个用户每天的签到用一位表示,一年的签到只需要365位。一个月最多只有31天,只需要31位。

比如2021年5月统计号89757的用户应该怎么打卡?

Key可以设计成uid:sign:{userId}:{yyyyMM},一个月中每一天的值-1可以作为偏移量。

第一步是执行以下指令来记录用户在2021年5月16日的打卡时间。

SETBIT uid:sign:89757:202105 15 1

第二步:确定用户号89757是否在2021年5月16日打卡。

GETBIT uid:sign:89757:202105 15

第三步:统计这个用户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开始。

连续签入用户的总数

在记录了1亿用户连续7天的打卡数据后,如何统计连续7天的连续打卡用户总数?

我们把每天的日期作为位图的键,把用户标识作为偏移量。如果我们打卡,我们将偏移位置的位设置为1。

对应的一组键的每一位的数据是用户在该日期的打卡记录。

总共有七个位图,如果可以和这七个位图对应的位。

相同的UserID偏移量是相同的。当一个userID在7位图对应的偏移位置bit = 1时,表示用户已经连续打卡7天。

结果保存在一个新的位图中,然后我们通过BITCOUNT统计bit = 1的个数,得到连续7天打卡的用户总数。

Redis提供指令Bitop操作Dest Key Key [key...],用于对一个或多个keys = key的位图进行按位操作。

运算可以是与、或、非、异或。当BITOP处理不同长度的字符串时,较短字符串的缺失部分将被视为0。空的键也被视为包含0的字符串序列。

简单易懂,如下图所示:

BITOP

3位图,并对相应的位进行运算,结果保存在新位图中。

操作指令指示三个位图进行“与”操作,结果保存在destmap中。然后在destmap上执行位计数统计。

//AND运算Bitop和destmap位图:01位图:02位图:03//Count位数= 1 BITCOUNT destmap

简单计算下一个1亿位位图的内存开销,大概占12 MB内存,7天位图的内存开销大概是84 MB。同时,我们最好设置位图的过期时间,让Redis删除过期的打卡数据,节省内存。

总结

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

只需要一位来表示0和1。在统计海量数据时,内存占用会大大减少。

 
友情链接
鄂ICP备19019357号-22