redis 系列之——数据类型 bitmaps:今天你签到了吗?

用户头像
诸葛小猿
关注
发布于: 2020 年 07 月 15 日
redis系列之——数据类型bitmaps:今天你签到了吗?

Redis系列目录



redis系列之——分布式锁



redis系列之——缓存穿透、缓存击穿、缓存雪崩



redis系列之——Redis为什么这么快?



redis系列之——数据持久化(RDB和AOF)



redis系列之——一致性hash算法



redis系列之——高可用(主从、哨兵、集群)



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;



127.0.0.1:6379> setbit active:2020-07-01 666 1 #userId=666的用户登陆,这是今天登陆的第一个用户。
(integer) 0
127.0.0.1:6379> setbit active:2020-07-01 100000000 1 #userId=100000000的用户登陆,这是今天第二个登陆的用户。
(integer) 0
127.0.0.1:6379> setbit active:2020-07-01 33 1
(integer) 0
127.0.0.1:6379> setbit active:2020-07-01 666 1
(integer) 0
127.0.0.1:6379> setbit active:2020-07-01 100000 1
(integer) 0



第一个用户登陆时,会新建一个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是否登陆:



127.0.0.1:6379> setbit active:2020-07-01 666
(integer) 1



返回为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该平台有多少用户活跃(登陆):



127.0.0.1:6379> bitcount active:2020-07-01 0 -1
(integer) 342342326



有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该怎么做,你是不是已经知道了?



完成,收工!!





传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。





发布于: 2020 年 07 月 15 日 阅读数: 158
用户头像

诸葛小猿

关注

我是诸葛小猿,一个彷徨中奋斗的互联网民工 2020.07.08 加入

公众号:foolish_man_xl

评论

发布
暂无评论
redis系列之——数据类型bitmaps:今天你签到了吗?