redis 系列之——数据类型 bitmaps:今天你签到了吗?
Redis系列目录
redis系列之——数据类型geospatial:你隔壁有没有老王?
redis系列之——数据类型bitmaps:今天你签到了吗?
平台日活跃100,000,000!!!今天有80,000,000人签到!!!
这个是怎么统计出来的?如果让你设计,你会如何设计?是不是使用mysql存储用户活跃的信息,同时使用select count(*)统计总数?数据量大,是不是准备做离线数据处理?实时性如何保证?有没有更简单的方式?
你也许已经知道Redis并不是简单的key-value存储,还有Hash、List、Set、Zset。
实际上Redis是一个数据结构服务器,支持不同类型的值。
今天给大家介绍redis中一种90%的程序员都不知道但是非常有用的数据类型,Bitmaps。
顺便回顾一下之前的两篇文章《redis系列之——缓存穿透、缓存击穿、缓存雪崩》、《布隆过滤器是个啥!》提到的bloom filter。
什么是 Bitmaps
来看看官方对Bitmaps的说明:
Bitmaps 并不是实际的数据类型,而是定义在String类型上的一个面向字节操作的集合。因为字符串是二进制安全的块,他们的最大长度是512M,最适合设置成2^32个不同字节。
bitmaps的位操作分成两类:1.固定时间的单个位操作,比如把String的某个位设置为1或者0,或者获取某个位上的值 2.对于一组位的操作,对给定的bit范围内,统计设定值为1的数目(比如人口统计)。
bitmaps最大的优势是在存储数据时可以极大的节省空间,比如在一个项目中采用自增长的id来标识用户,就可以仅用512M的内存来记录40亿用户的信息(比如用户是否希望收到新的通知,用1和0标识)
简单来说bitmaps就是一个长度可变的bit数组。每个位只能存储0或1。我们先来看看bitmap的具体表示,当我们使用命令 setbit key (0,2,4,6) 1后,这个bit数组的具体表示为:
| bit0 | bit1 | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
一天的1亿人的登录情况(登录、未登录)就可以使用一个长度为1亿的bit数组存储,数组的索引就是用户的userId(假设userId是自增的)。
如何使用
这里介绍几个常用的bitmaps的操作命令。
SETBIT key offset value
该命令起始版本:2.2.0;时间复杂度:O(1);返回值:在offset处原来的bit值;该命令的作用:设置或者清空key的value(字符串)在offset处的bit值。
那个位置的bit要么被设置,要么被清空,这个由value(只能是0或者1)来决定。当key不存在的时候,就创建一个新的字符串value。要确保这个字符串大到在offset处有bit值。参数offset需要大于等于0,并且小于2^32(限制bitmap大小为512MB)。当key对应的字符串增大的时候,新增的部分bit值都是设置为0。
警告:对于一个不存在的key,首次使用setbit命令在offset=2^32-1的位置(最后一个bit位)设置value时,或者对于一个存在的key,并非首次使用setbit命令在 offset=2^32-1 的位置设置value但之前设置offset的位置比较小时, Redis需要立即分配所有内存,这有可能会导致服务阻塞一会。在一台2010MacBook Pro上,offset为2^ 32-1(分配512MB)需要~300ms,offset为2^ 30-1(分配128MB)需要~80ms,offset为2^ 28-1(分配32MB)需要~30ms,offset为2^26-1(分配8MB)需要8ms。注意,一旦第一次内存分配完,后面对同一个key调用SETBIT就不会预先得到内存分配。
比如,某个平台需要统计每天用户的活跃情况(一个用户当天登了就算是一个当天活跃用户),每天都需要新生成一个bitmaps存储当天用户的登录情况。
这里的设计是: setbit active:{date} {userId} value ;比如存储2020-07-01该平台的用户活跃情况,key=active:2020-07-01;
第一个用户登陆时,会新建一个String,这个String被分配的空间时667bit,这是offset为0到665的用户未登录,值为0;
第二个用户登陆时,这个String已经存在,这时会被分配666到100000000的字节空间,这个时候就会出现短时间阻塞。
GETBIT key offset
该命令起始版本:2.2.0;时间复杂度:O(1);返回值:在offset处的bit值;该命令的作用:返回key对应的string在offset处的bit值。
当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。当key不存在的时候,它就认为是一个空字符串,所以offset总是超出范围,然后value也被认为是由0比特填充的连续空间。
比如,查询userId=666的用户在2020-07-01是否登陆:
返回为1,表示登陆了。
BITCOUNT key [start end]
该命令起始版本:2.6.0;时间复杂度:O(N);该命令的作用:统计字符串被设置为1的bit数;返回值:被设置为 1 的位的数量。
一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置和 GETRANGE 命令类似,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,以此类推。不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
比如,统计2020-07-01该平台有多少用户活跃(登陆):
有342342326个用户登陆过。
处理上面介绍的三个命令,对bitmaps操作的命令还有很多,因为bitmaps的本质是String,相关的操作命令和string一样,可以参考官网:http://www.redis.cn/commands.html#string
除了上面的命令,还可以通过相关的命令比较两个string的差异,对两个string做与、或、非运算;有很多实际的场景都可以通过这些简单的命令实现。
使用场景
由于bit数组的每个位置只能存储0或者1这两个状态;所以对于实际生活中,处理两个状态的业务场景就可以考虑使用bitmaps。如用户登陆/未登录,签到/未签到,关注/未关注,打卡/未打卡等。同时bitmap还通过了相关的统计方法进行快速统计。
内存占用比较
假如一个平台有8亿用户,平均日活跃用户有1亿,,分别使用List和Bitmap存储平台某一天是否活跃(登陆)用户时内存占用情况(1KB=1000bit):
| 数据类型 | 每个userId占用空间 | 需要存储的用户量 | 内存使用总量 |
| :------- | :-------------------------------------- | ---------------- | -------------------------- |
| List | 4 8bit=32bit(假设userId用的是int存储) | 100,000,000 | 32bit 100,000,000= 400MB |
| Bitmaps | 1bit | 800,000,000 | 1bit * 800,000,000=100MB |
假如一个平台有8亿用户,平均日活跃用户有100万,,分别使用List和Bitmap存储平台某一天是否活跃(登陆)用户时内存占用情况(1KB=1000bit):
| 数据类型 | 每个userId占用空间 | 需要存储的用户量 | 内存使用总量 |
| :------- | :-------------------------------------- | ---------------- | ------------------------ |
| List | 4 8bit=32bit(假设userId用的是int存储) | 1,000,000 | 32bit 1,000,000= 4MB |
| Bitmaps | 1bit | 800,000,000 | 1bit * 800,000,000=100MB |
所以并不是在所有的情况下,使用bitmap都是最好的选择。平台虽然有8亿用户,但是活跃的用户很少,这是使用Bitmaps,如果只有一个用户登录(加入是userId=800,000,000-1这个用户登录),也需要分配100MB的空间。
注意
bitmaps类型(string)最大长度为512M。
setbit时的偏移量很大时,可能会有较大耗时。
bitmaps不是绝对的好,有时可能更浪费空间。
Bloot Filter该怎么做,你是不是已经知道了?
完成,收工!!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。
版权声明: 本文为 InfoQ 作者【诸葛小猿】的原创文章。
原文链接:【http://xie.infoq.cn/article/85bb5725442408acf968455ba】。文章转载请联系作者。
评论