Bitmap(位图)以占用空间下极小,查询速度快的优势,主要应用于各种“是非”类二值情况的统计或判断。
Bitmap类型的常用操作
SETBIT 添加Bitmap
127.0.0.1:6379> SETBIT bitmap 10 1
(integer) 0
127.0.0.1:6379> SETBIT bitmap 8 1
(integer) 0
127.0.0.1:6379> SETBIT bitmap 3 1
(integer) 0
这里返回值 是偏移位的原始值(不是修改之后的值)
127.0.0.1:6379> SETBIT bitmap 3 1 # 再次设置,返回的之前的 1
(integer) 1
127.0.0.1:6379> SETBIT bitmap 3 0 # 再次修改值为0,返回的是之前设置的 1
(integer) 1
GETBIT 查询Bitmap
127.0.0.1:6379> GETBIT bitmap 3
(integer) 0
127.0.0.1:6379> GETBIT bitmap 8
(integer) 1
127.0.0.1:6379> GETBIT bitmap 100 # 之前没有设置过的位,默认值都是0
(integer) 0
BITCOUNT 统计bitmap中为1的个数
这里重新设置之前的位图,满足下面这样的结构
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
127.0.0.1:6379> DEL bitmap # 删除key
(integer) 1
127.0.0.1:6379> SETBIT bitmap 3 1
(integer) 0
127.0.0.1:6379> SETBIT bitmap 8 1
(integer) 0
127.0.0.1:6379> SETBIT bitmap 10 1
(integer) 0
127.0.0.1:6379> BITCOUNT bitmap # 统计所有范围
(integer) 3
127.0.0.1:6379> BITCOUNT bitmap 0 10 # 统计0字节到10字节 1的个数
(integer) 3
127.0.0.1:6379> BITCOUNT bitmap 1 10 # 统计1字节到10字节 1的个数
(integer) 2
127.0.0.1:6379> BITCOUNT bitmap 2 10 # 统计2字节到10字节 1的个数
(integer) 0
127.0.0.1:6379> BITCOUNT bitmap 0 0 # 统计2字节到0字节 1的个数
(integer) 1
可以看出局部范围统计的参数是以字节为单位(8bit)
BITPOS 指定范围内第一个0或1的偏移位
还是以这个结构为初始值
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
127.0.0.1:6379> BITPOS bitmap 1 # 全部范围下第一个为1的偏移位
(integer) 3
127.0.0.1:6379> BITPOS bitmap 0
(integer) 0
127.0.0.1:6379> BITPOS bitmap 1 1 2 # 1字节到2字节中 第一个为1的偏移位
(integer) 8
BITOP 对多个Bitmap 进行位操作,并保存到新的Bitmap中
操作可以有AND、OR、XOR 或者 NOT
设置3bitmap如下结构
bit1:
0 | 1 | 2 | 3 |
---|---|---|---|
1 | 0 | 0 | 0 |
bit2:
0 | 1 | 2 | 3 |
---|---|---|---|
0 | 1 | 0 | 0 |
bit3:
0 | 1 | 2 | 3 |
---|---|---|---|
0 | 0 | 1 | 0 |
127.0.0.1:6379> SETBIT bit1 0 1
(integer) 0
127.0.0.1:6379> SETBIT bit2 1 1
(integer) 0
127.0.0.1:6379> SETBIT bit3 2 1
(integer) 0
127.0.0.1:6379> BITOP AND andbit bit1 bit2 bit3 # 对3个bitmap执行AND操作 并保存到andbit
(integer) 1
127.0.0.1:6379> GETBIT andbit 0
(integer) 0
127.0.0.1:6379> GETBIT andbit 1
(integer) 0
127.0.0.1:6379> GETBIT andbit 2
(integer) 0
127.0.0.1:6379> BITCOUNT andbit
(integer) 0
可以看出操作后andbit 最后的结构应该是
0 | 1 | 2 | 3 |
---|---|---|---|
0 | 0 | 0 | 0 |
实际应用场景
1.用户当天是否签到/打卡/登录
这类基本只需要记录二值的功能都可以用bitmap实现,以用户id(数字类型)作为offset。
(比如用户ID从1开始,即使最大用户id到1个亿,那么记录1个bitmap 最多也只需要12mb的内存)
每天记录一个打卡的key,每个用户打卡就设置一个bit
127.0.0.1:6379> SETBIT mark:20240525 100 1 # 5月25号 用户100打卡
(integer) 0
127.0.0.1:6379> SETBIT mark:20240525 130 1
(integer) 0
127.0.0.1:6379> SETBIT mark:20240525 135 1
(integer) 0
如果需要判断用户某天是否打过卡这个状态
只需要查询一下某天对应的bitmap是否有对应位
127.0.0.1:6379> GETBIT mark:20240525 135
(integer) 1
127.0.0.1:6379> GETBIT mark:20240525 134
(integer) 0
SETBIT和GETBIT 执行的时间复杂度都是O(1) 所以速度是很快的
当然对于登出这种反向操作的也可以设置为0
还可以继续扩展 判断某个用户是否在一段时间内连续打卡
127.0.0.1:6379> SETBIT mark:20240526 100 1 # 5月26号 用户100再次打卡
(integer) 0
127.0.0.1:6379> BITOP AND and:mark mark:20240525 mark:20240526
(integer) 17
127.0.0.1:6379> GETBIT and:mark 100 # 用户100 在25和26号都打了卡
(integer) 1
127.0.0.1:6379> GETBIT and:mark 130 # 用户130 没有连续打卡
(integer) 0
2.当前在线人数
对于某个应用/游戏,或者某个视频当前在线观看人数的统计
针对这类场景也是同样以用户ID为offset
127.0.0.1:6379> SETBIT app:online 100 1 # 用户100在线 登记
(integer) 0
127.0.0.1:6379> SETBIT app:online 130 1
(integer) 0
127.0.0.1:6379> SETBIT app:online 135 1
(integer) 0
127.0.0.1:6379> BITCOUNT app:online # 这一时刻在线用户数
(integer) 3
127.0.0.1:6379> SETBIT app:online 130 0 # 130用户下线 登记
(integer) 1
127.0.0.1:6379> BITCOUNT app:online
(integer) 2
当然上面是实现统计的基本原理,对于实际场景中还需要考虑一些情况,比如用户下线并不会通知系统(系统强制退出/断网/关机等),这样就无法感知到用户是否下线。
一般在线的统计是需要客户端定时上报是否在线的(比如每隔60s上报一次在线情况)
这样我们在统计某个应用在线用户数的时候,可以直接清空原来的bitmap, 让用户重新上报在线情况。
不过这样粗暴的解决也会存在一个问题,删除key后,由于每个客户端上报的周期不一样(最多要60s才能将所有真正的在线用户全部上报上来),这样在60s之内去获取在线人数 肯定会比实际的少。
时间节点1:
在线人数4个
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
时间节点2:
id:5异常退出后
在线人数还是4个
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
时间节点3:
删除key
用户:1 上报在线 用户:3 用户:7 还未到下个客户端上报周期
统计到的在线用户数是1,实际上应该是3
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
为了优化这个问题,有以下一些解决方案:
缩短客户端上报在线状态的时间间隔
这个方案可以降低统计数据与真实数据间的差距,但是会增加服务或者网络额外的开销
按时间段统计上报
比如客户端上报周期是60s,
10:00~10:01时间段,写入的key为app:online:1000
10:01~10:02时间段,写入的key为app:online:1001
但是查询在线人数时候,还是使用上一周期的key(app:online:1000)
对于主动上报离线的用户, 还是往上一周期的key(app:online:1000)去上报(当前时间段的key为默认位就是0)
这样做的一个前提是,我们需要牺牲数据的实时性(延迟60s左右,一般的业务也是允许的),同时也增加了空间的使用,不过我们可以增加清理策略,比如在第三个时间段10:02~10:03时,删除app:online:1000的key(当然为了后续统计方便,也可以同时在其他数据库记录这个聚合后的值再删除)。
补充
Bitmap底层是基于String类型存储的(string底层以二进制格式存储)
127.0.0.1:6379> SETBIT bit 0 1
(integer) 0
127.0.0.1:6379> get bit # 使用get命令
"\x80"
反过来也可以,比如“3”的底层存储是
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
127.0.0.1:6379> SET stringbit 3 # 写入string类型的3
OK
127.0.0.1:6379> GETBIT stringbit 0
(integer) 0
127.0.0.1:6379> GETBIT stringbit 1
(integer) 0
127.0.0.1:6379> GETBIT stringbit 2
(integer) 1
127.0.0.1:6379> GETBIT stringbit 3
(integer) 1
127.0.0.1:6379> GETBIT stringbit 4
(integer) 0
127.0.0.1:6379> GETBIT stringbit 5
(integer) 0
127.0.0.1:6379> GETBIT stringbit 6
(integer) 1
127.0.0.1:6379> GETBIT stringbit 7
(integer) 1