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位;
- 核心优势是空间效率和位操作原子性;
- 适合海量布尔值存储与统计的场景。
