当前位置: 首页 > 科技观察

Redis实战篇:使用Bitmap实现亿级海量数据统计

时间:2023-03-12 11:02:31 科技观察

在移动应用的业务场景中,我们需要保存这样的信息:一个key关联一个数据集。常见的场景如下:给一个userId,判断用户的登录状态;显示用户在某月的签到次数和首次签到时间;近7天2亿用户签到情况,统计7天内连续登录用户总数;通常,我们面临的用户数量和访问量都是巨大的,比如百万级、千万级的用户量,或者是千万级甚至亿级的访问信息量。因此,我们必须选择一种能够非常高效地统计大量数据(例如数十亿)的集合类型。如何选择合适的数据集,首先要了解常用的统计模型,并使用合理的数据类型来解决实际问题。四种统计类型:二进制状态统计;汇总统计;排序统计;基数统计。本文将使用二进制状态统计类型作为实战系列的开篇,本文将使用除String、Set、Zset、List、hash之外的扩展数据类型Bitmap。文中涉及的指令可以通过在线Redis客户端运行调试,地址:https://try.redis.io/,超级方便。Message多分享多付出,前期为他人创造更多价值,不在乎回报。从长远来看,这些努力会给你带来成倍的回报。尤其是刚开始和别人合作的时候,不要担心短期的回报,没有多大意义,更多的是锻炼自己的眼光、观点和解决问题的能力。二进制状态统计代码兄弟,什么是二进制状态统计?即集合中元素的值只有0和1。在签到签到以及用户是否登录的场景中,只需要记录sign-in(1)或未登录(0)、已登录(1)或未登录(0)。如果我们在判断用户是否登录的场景中使用Redis的String类型(key->userId,value->0表示离线,1-登录),如果我们存储100万用户的登录状态,如果string的存储形式,需要存储100万个字符串,内存开销太大。代码兄,为什么String类型的内存开销大呢?String类型除了记录实际数据外,还需要额外的内存来记录数据长度、空间占用等信息。当保存的数据中包含字符串时,使用简单的动态字符串(SDS)结构保存String类型,如下图:SDSlen:占用4个字节,表示buf使用的长度。alloc:4字节,表示buf实际分配的长度,通常>len。buf:字节数组,保存实际数据,Redis自动在数组末尾添加一个“\0”,额外占用一个字节的开销。因此,在SDS中,除了buf中保存实际数据外,len和alloc都是额外的开销。另外还有RedisObject结构的开销,因为Redis有很多数据类型,不同的数据类型有相同的元数据来记录(比如上次访问的时间,引用次数等)。因此,Redis会使用一个RedisObject结构来统一记录这些元数据,同时指向实际的数据。对于二进制状态的场景,我们可以使用Bitmap来实现。比如我们用一个bit来表示登录状态,1亿个用户只占用1亿个bit的内存≈(100000000/8/1024/1024)12MB。空间占用的大概计算公式为:($offset/8/1024/1024)MB什么是Bitmap?Bitmap的底层数据结构使用String类型的SDS数据结构来存储位数组,而Redis存储每个字节数组的8个位被利用,每个位代表一个元素的二进制状态(0或1)。Bitmap可以看作是一个位单元数组,数组的每个单元只能存储0或1,数组的下标在Bitmap中称为偏移量。为了直观显示,我们可以理解为buf数组的每个字节用一行来表示,每一行有8位,8个格子代表这个字节中的8位,如下图所示:Bitmap8bitsFormaByte,所以Bitmap会大大节省存储空间。这就是Bitmap的优势。判断用户登录状态如何在大量用户中使用Bitmap判断用户是否在线?Bitmap提供了GETBIT和SETBIT操作,通过一个偏移值offset来读写bit数组的offset位置的bit,需要注意的是offset是从0开始的。只需要一个key=login_status来存储用户登录状态集合数据,以用户ID为偏移量,在线时置1,离线时置0。使用GETBIT判断对应用户是否在线。5亿用户只需要6MB的空间。SETBIT命令SETBIT设置或清除offset处key值的位值(只能为0或1)。GETBIT命令GETBIT获取key在offset位的值,当key不存在时返回0。如果我们要判断ID=10086的用户登录状态:第一步,执行如下命令表示用户已经登录。SETBITlogin_status100861第二步,判断用户是否登录,返回值1表示登录。GETBITlogin_status10086第三步为退出,将offset对应的值设置为0。1比特,一年的签到只需要365比特。一个月最多只有31天,只需要31位。比如统计号为89757的用户在2021年5月应该怎么打卡?key可以设计成uid:sign:{userId}:{yyyyMM},月中每一天的值-1可以作为offset(因为offset是从0开始的,所以offset=date-1).第一步执行如下命令,记录用户2021年5月16日签到。SETBITuid:sign:89757:202105151第二步判断89757号用户是否在2021年5月16日签到。GETBITuid:sign:89757:20210515第三步统计用户5月份的签到次数,使用BITCOUNT命令。该命令用于计算给定位数组中值为1的位数。BITCOUNTuid:sign:89757:202105这样我们就可以实现用户每月的签到情况,是不是很棒?如何统计本月首次签到时间?Redis提供了BITPOSkeybitValue[start][end]命令,返回的数据表示Bitmap中第一个值为bitValue的偏移位置。默认情况下,该命令将检测整个位图,用户可以通过可选的开始参数和结束参数指定要检测的范围。所以我们可以通过执行如下命令获取userID=89757的首次登录日期:BITPOSuid:sign:89757:2021051需要注意的是,我们需要返回值+1,因为偏移量是从0开始的连续登录用户总数连续7天记录了1亿用户的登录数据。如何统计连续7天签到用户总数?我们将每一天的日期作为Bitmap的key,userId作为offset,如果是签到,则将offset位置的bit设置为1。集合中的每一bit数据对应key是用户在该日期的签到记录。这样的Bitmap一共有7个,如果我们可以对这7个Bitmap的对应位进行“与”运算。同一个UserID的offset是一样的。当一个userID在7个Bitmap对应的偏移位置bit=1时,表示该用户连续登录7天。结果保存在一个新的Bitmap中,我们通过BITCOUNT统计bit=1的个数,得到连续7天登录的用户总数。Redis提供了BITOP操作destkeykey[key...]该命令用于对一个或多个key=key的Bitmap进行位操作。运算可以是and、OR、NOT、XOR。BITOP在处理不同长度的字符串时,将较短的字符串缺失的部分作为0处理。一个空的key也被看做是一串包含0的字符串,很容易理解,如下图:BITOP有3个Bitmap,对应的位进行“与”运算,结果保存在一个新的Bitmap中.运算指令是指对三个位图进行AND运算,并将结果保存到destmap中。然后对destmap进行BITCOUNT统计。//用BITOPANDdestmap操作bitmap:01bitmap:02bitmap:03//统计位数=1BITCOUNTdestmap简单计算下一个1亿位Bitmap占用的内存开销,约占12MB内存(10^8/8/1024/1024),一个7天的Bitmap的内存开销大约是84MB。同时我们最好给Bitmap设置一个过期时间,让Redis删除过期的签到数据以节省内存。总结的思想最重要。当我们遇到只需要统计数据二进制状态的统计场景,比如用户是否存在,ip是否被列入黑名单,签到签到统计等,我们可以考虑使用Bitmap。只需要一位来表示0和1,在统计海量数据时,内存占用会大大减少。