Appearance
Bitmap详解
一、Bitmap是什么?
Bitmap是Redis提供的位级操作数据类型,本质是二进制位的集合(每个位只能是0
或1
)。它通过**字符串(SDS)**作为底层存储结构,将每个字节(8位)拆分为独立的位,实现对海量布尔值(是/否、存在/不存在)的高效存储与查询。
1. 核心特点
- 空间效率极高:1位(bit)存储1个布尔值,例如存储1亿个用户的在线状态,仅需
1亿/8 = 12.5MB
(远小于集合Set
的400MB+)。 - 位操作原子性:所有位操作命令(如
SETBIT
、BITCOUNT
)都是原子的,无需担心并发冲突。 - 支持丰富的位运算:包括位设置(
SETBIT
)、位获取(GETBIT
)、位计数(BITCOUNT
)、位合并(BITOP
)、位查找(BITPOS
)等。
二、Bitmap底层原理
Bitmap的底层依赖Redis的字符串类型(SDS,简单动态字符串),每个字符串的字节(char
)对应8个二进制位。以下是关键原理的拆解:
1. 位索引与字节映射
Bitmap的位索引从0开始,每个字节的8位对应连续的位索引。例如:
- 字节0(第1个字节)对应位索引
0~7
(位0是字节的最低位,位7是最高位); - 字节1(第2个字节)对应位索引
8~15
; - 字节n对应位索引
8n ~ 8n+7
。
示例:字符串"\x03"
(二进制00000011
)对应的位状态:
位索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
位值 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2. 动态扩展机制
当设置的位索引超过当前字符串长度时,Redis会自动扩展字符串,填充0
(默认值)。扩展逻辑如下:
- 计算需要的字节数:
ceil(offset + 1 / 8)
(offset
是位索引,从0开始); - 分配新的SDS空间(根据Redis的扩容策略,通常会预留额外空间以减少后续扩容次数);
- 将原数据复制到新空间,并填充
0
至扩展的字节; - 更新SDS的
len
(当前长度)和free
(剩余空间)字段。
示例:当前字符串长度为2字节(16位),执行SETBIT key 20 1
(设置位20为1):
- 需要的字节数:
ceil(21/8) = 3
字节(位20对应第3个字节的第4位:20 = 8*2 +4
); - 扩展字符串至3字节,前2字节保留原数据,第3字节填充
0
; - 将第3字节的第4位(
1<<4 = 16
)设置为1,最终第3字节的值为0x10
(二进制00010000
)。
3. 核心命令的底层实现
Bitmap的所有命令都基于**位掩码(Bit Mask)**操作,以下是关键命令的原理拆解:
(1)SETBIT:设置位值
- 命令格式:
SETBIT key offset value
(value
只能是0或1); - 底层逻辑:
- 计算位对应的字节位置:
byte_pos = offset / 8
; - 计算位对应的字节内偏移:
bit_pos = offset % 8
; - 生成位掩码:
mask = 1 << bit_pos
(例如bit_pos=2
,掩码是00000100
); - 修改字节值:
- 若
value=1
:用OR
操作(byte |= mask
),将目标位设为1; - 若
value=0
:用AND
操作(byte &= ~mask
),将目标位设为0;
- 若
- 写回字节至SDS,并更新字符串长度(若需要扩展)。
- 计算位对应的字节位置:
示例:SETBIT key 10 1
(设置位10为1):
byte_pos = 10 /8 = 1
(第2个字节);bit_pos =10 %8 =2
(字节内第2位);- 掩码:
1<<2 =4
(00000100
); - 若原字节值为
0x00
(00000000
),则修改后为0x04
(00000100
)。
(2)GETBIT:获取位值
- 命令格式:
GETBIT key offset
; - 底层逻辑:
- 计算
byte_pos
和bit_pos
(同SETBIT
); - 若
byte_pos
超过字符串长度,返回0
(未设置的位默认是0); - 生成掩码
mask =1 << bit_pos
; - 用
AND
操作(byte & mask
)提取目标位:若结果非0,返回1
,否则返回0
。
- 计算
示例:GETBIT key 10
(获取位10的值):
- 若字节1的值为
0x04
(00000100
),则0x04 & 4 =4
(非0),返回1
。
(3)BITCOUNT:统计1的个数
- 命令格式:
BITCOUNT key [start end]
(start
和end
是字节索引,默认统计全部位); - 底层逻辑: Redis使用预计算查表法(
bitcount_table
)加速统计,该表存储了每个字节(0~255)的1的个数(例如bitcount_table[0x0f] =4
,bitcount_table[0xff] =8
)。- 遍历指定范围内的每个字节;
- 查表获取该字节的1的个数,累加得到总次数。
优化:对于大范围(如超过1024字节),Redis会使用64位整数的位计数指令(如x86的popcnt
指令),将数据分成64位块,快速统计每个块的1的个数,进一步提升速度。
示例:字符串"\x03"
(00000011
)的BITCOUNT
结果为2
(位0和位1为1)。
(4)BITOP:位运算(AND/OR/XOR/NOT)
- 命令格式:
BITOP OP dstkey srckey1 srckey2 ...
(OP
可以是AND
、OR
、XOR
、NOT
); - 底层逻辑:
- 计算输出字符串的长度:
max(len(srckey1), len(srckey2), ...)
(NOT
操作的输出长度等于输入长度); - 逐个字节处理:
- 对于
AND
/OR
/XOR
:取每个输入字符串的对应字节(若输入字符串长度不足,视为0
),执行位运算; - 对于
NOT
:取输入字符串的对应字节,执行取反操作(~byte
);
- 对于
- 将结果字节写入输出字符串(
dstkey
)。
- 计算输出字符串的长度:
示例:BITOP OR dst a b
(合并a
和b
的位,取或):
a
的字符串为"\x15"
(00010101
,位0、2、4为1);b
的字符串为"\x2c"
(00101100
,位2、3、5为1);- 或运算后,
dst
的字符串为"\x3d"
(00111101
,位0、2、3、4、5为1)。
(5)BITPOS:查找第一个0或1的位置
- 命令格式:
BITPOS key bit [start end]
(bit
是0或1,start
和end
是字节索引); - 底层逻辑: Redis使用二分查找+块遍历加速查找:
- 二分查找找到第一个包含目标位的块(如每8字节一块);
- 在该块内遍历每个字节,找到第一个包含目标位的字节;
- 在该字节内遍历每个位(从位0到位7),找到第一个目标位,计算其全局索引。
示例:字符串"\x0b"
(00001011
,位0、1、3为1)的BITPOS key 0
结果为2
(第一个0的位置是位2)。
三、Bitmap的优势
- 空间效率:1位存储1个布尔值,远优于其他数据类型(如
Set
的4字节/元素)。 - 时间效率:位操作是原子的,且命令的时间复杂度低(
SETBIT
/GETBIT
为O(1)
,BITCOUNT
/BITOP
为O(n)
,n
是处理的字节数)。 - 功能丰富:支持位设置、获取、统计、合并、查找等操作,满足多种场景需求。
四、Bitmap的适用场景
Bitmap适合存储海量布尔值且需要高效统计的场景,以下是典型案例:
1. 用户签到系统
- 设计:用
sign:user:uid
作为key,offset
表示日期(如从2025-01-01开始的天数),1
表示签到,0
表示未签到。 - 操作:
- 签到:
SETBIT sign:user:123 191 1
(2025-07-11是第191天); - 统计当月签到次数:
BITCOUNT sign:user:123 24 26
(假设7月有31天,对应字节索引24~26,共31位); - 查找第一个未签到日期:
BITPOS sign:user:123 0 24 26
。
- 签到:
2. 活跃用户统计
- 设计:用
active:yyyyMMdd
作为key,offset
表示用户ID,1
表示活跃。 - 操作:
- 统计周活跃用户:
BITOP OR active:week active:20250706 active:20250707 ... active:20250712
,然后BITCOUNT active:week
; - 统计同时活跃在周一和周二的用户:
BITOP AND active:monday-tuesday active:20250707 active:20250708
,然后BITCOUNT
。
- 统计周活跃用户:
3. 用户在线状态
- 设计:用
online
作为key,offset
表示用户ID,1
表示在线,0
表示离线。 - 操作:
- 获取用户在线状态:
GETBIT online 123
; - 统计在线人数:
BITCOUNT online
。
- 获取用户在线状态:
4. 布隆过滤器(Bloom Filter)
- 设计:用Bitmap实现布隆过滤器,用于快速判断元素是否在集合中(有一定误判率)。
- 原理:对元素进行多次哈希,将对应的位设为1;查询时,若所有哈希位都为1,则元素可能存在(否则一定不存在)。
五、Bitmap的局限性
- 偏移量限制:虽然Redis支持
2^32-1
的偏移量(约4GB位),但过大的偏移量会导致内存浪费(如设置偏移量为1亿,需要12.5MB空间,即使大部分位是0)。 - 范围查询效率:遍历所有位(如查找所有签到日期)的效率较低,适合统计而非明细查询。
- 复杂查询麻烦:例如查找连续签到超过7天的用户,需要结合
BITOP
和BITPOS
命令,逻辑较复杂。
六、总结
Bitmap是Redis中最高效的数据类型之一,通过位级操作实现了高空间效率和高时间效率,适合处理海量布尔值的场景。其底层依赖字符串(SDS),通过位索引与字节映射、动态扩展、预计算查表等机制,保证了操作的高效性。
理解Bitmap的原理,能帮助我们在用户签到、活跃统计、在线状态等场景中,选择最合适的数据类型,发挥Redis的最大性能。
关键结论:
- Bitmap的本质是字符串的位级使用;
- 位索引从0开始,每个字节对应8位;
- 核心优势是空间效率和位操作原子性;
- 适合海量布尔值存储与统计的场景。