Redis Bitmap
Redis位图(Bitmap)是一种基于字符串类型的高效数据结构,它通过将字符串中的每个字节拆分为8个二进制位(bit),利用这些bit位来表示和存储布尔类型的信息(0或1)。相比传统的字符串、哈希等结构,Bitmap在存储海量布尔型数据时具有极高的空间效率,同时支持高效的位运算操作,广泛应用于用户签到、在线状态判断、数据统计等场景。
# 一、核心概念
# 1.1 本质
Bitmap的本质是Redis的字符串(String)类型,因为Redis的字符串是二进制安全的,其底层存储的是字节序列。Bitmap通过对这些字节序列的bit位进行操作,实现了“以bit为单位”的数据存储和管理。例如,一个长度为1的字符串(1字节)可以表示8个bit位,对应8个布尔值;长度为2的字符串可以表示16个bit位,以此类推。
# 1.2 核心优势
- 空间效率极高:1个字节可存储8个布尔值,相比使用哈希表(Hash)存储(每个键值对至少占用几十字节),空间占用降低一个数量级。例如,存储1000万用户的签到状态,仅需约1.2MB(10000000 / 8 / 1024 ≈ 1220KB)。
- 操作高效:支持单个bit位的快速设置、查询,以及多个Bitmap之间的位运算(与、或、异或等),所有操作的时间复杂度均为O(1)或O(n)(n为bit位长度,效率远高于其他结构)。
- 兼容性好:基于String类型实现,无需额外的底层存储支持,所有支持Redis String的客户端均可直接操作Bitmap。
# 1.3 适用场景特征
适合存储“是否”型数据(如是否签到、是否在线、是否读过某篇文章),且数据标识为连续或可枚举的整数(如用户ID、日期偏移量、商品ID)。若数据标识为非整数(如用户名、UUID),需先进行映射转换为整数,否则无法高效使用Bitmap。
# 二、核心命令详解
Redis提供了一系列专门用于操作Bitmap的命令,涵盖bit位的设置、查询、统计、位运算等核心功能。以下是常用命令的详细说明:
# 2.1 SETBIT:设置指定bit位的值
# 2.1.1 语法
SETBIT key offset value
# 2.1.2 参数说明
- key:Bitmap对应的键名。
- offset:bit位的偏移量,从0开始(0表示第一个bit位),支持非负整数(理论上无上限,仅受Redis内存限制)。
- value:待设置的bit值,只能是0或1。
# 2.1.3 功能
将key对应的Bitmap中,偏移量为offset的bit位设置为value。若key不存在,会先创建一个空字符串(所有bit位默认值为0);若offset超过当前字符串的长度,Redis会自动在字符串末尾补0,直到长度足以覆盖offset对应的bit位。
# 2.1.4 返回值
该bit位在设置前的旧值(0或1)。
# 2.1.5 示例
# 设置用户ID为100的用户在2026-01-13的签到状态为1(已签到)
# 键名:sign:20260113(表示2026年01月13日的签到表)
# 偏移量:100(用户ID),值:1
127.0.0.1:6379> SETBIT sign:20260113 100 1
(integer) 0 # 该bit位之前为0(未签到)
# 再次设置同一偏移量,返回旧值1
127.0.0.1:6379> SETBIT sign:20260113 100 1
(integer) 1
2
3
4
5
6
7
8
9
# 2.2 GETBIT:获取指定bit位的值
# 2.2.1 语法
GETBIT key offset
# 2.2.2 参数说明
- key:Bitmap对应的键名。
- offset:bit位的偏移量(从0开始)。
# 2.2.3 功能
获取key对应的Bitmap中,偏移量为offset的bit位的值。若key不存在,视为所有bit位为0的Bitmap;若offset超过当前字符串的长度,返回0。
# 2.2.4 返回值
指定bit位的值(0或1)。
# 2.2.5 示例
# 获取用户ID100在2026-01-13的签到状态
127.0.0.1:6379> GETBIT sign:20260113 100
(integer) 1 # 已签到
# 获取不存在的用户ID200的签到状态(返回0)
127.0.0.1:6379> GETBIT sign:20260113 200
(integer) 0
2
3
4
5
6
7
# 2.3 BITCOUNT:统计指定范围内的1的个数
# 2.3.1 语法
BITCOUNT key [start end]
# 2.3.2 参数说明
- key:Bitmap对应的键名。
- start/end:可选参数,指定统计的字节范围(注意:是字节范围,不是bit位范围),默认统计整个字符串的所有字节。start和end支持负数,-1表示最后一个字节,-2表示倒数第二个字节,以此类推。
# 2.3.3 功能
统计key对应的Bitmap中,指定字节范围内所有bit位中值为1的个数。若需要统计指定bit位范围,需先将bit位范围转换为字节范围(例如,统计bit 0-15,对应字节0-1)。
# 2.3.4 返回值
指定范围内1的个数。
# 2.3.5 示例
# 统计2026-01-13的总签到人数(所有bit位中1的个数)
127.0.0.1:6379> BITCOUNT sign:20260113
(integer) 568 # 当天有568人签到
# 统计前10个字节(对应bit 0-79)中1的个数(即用户ID 0-79中的签到人数)
127.0.0.1:6379> BITCOUNT sign:20260113 0 9
(integer) 32
2
3
4
5
6
7
# 2.4 BITOP:对多个Bitmap执行位运算
# 2.4.1 语法
BITOP operation destkey key [key ...]
# 2.4.2 参数说明
- operation:位运算类型,支持AND(与)、OR(或)、XOR(异或)、NOT(非)。注意:NOT运算仅支持单个key,其他运算支持多个key。
- destkey:运算结果存储的目标键名(会覆盖已有键)。
- key [key ...]:参与运算的源Bitmap键名。
# 2.4.3 功能
对多个源Bitmap执行指定的位运算,将结果存储到destkey中。当多个源Bitmap的长度不一致时,Redis会以最长的Bitmap为准,较短的Bitmap在末尾补0后再进行运算。
# 2.4.4 返回值
目标键destkey对应的字符串长度(以字节为单位)。
# 2.4.5 示例
# 场景:统计2026-01-13和2026-01-14两天都签到的用户(交集)
# 1. 假设已有两天的签到Bitmap:sign:20260113、sign:20260114
# 2. 执行AND运算,结果存入sign:20260113_14_both
127.0.0.1:6379> BITOP AND sign:20260113_14_both sign:20260113 sign:20260114
(integer) 1250 # 结果Bitmap长度为1250字节(对应10000个bit位,即支持10000个用户ID)
# 3. 统计两天都签到的人数
127.0.0.1:6379> BITCOUNT sign:20260113_14_both
(integer) 320 # 320人连续两天签到
# 场景:统计2026-01-13或2026-01-14至少签到一天的用户(并集)
127.0.0.1:6379> BITOP OR sign:20260113_14_either sign:20260113 sign:20260114
(integer) 1250
127.0.0.1:6379> BITCOUNT sign:20260113_14_either
(integer) 780 # 780人至少签到一天
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.5 BITPOS:查找指定值的第一个bit位偏移量
# 2.5.1 语法
BITPOS key value [start end]
# 2.5.2 参数说明
- key:Bitmap对应的键名。
- value:要查找的bit值(0或1)。
- start/end:可选参数,指定查找的字节范围(默认查找整个字符串),支持负数(-1表示最后一个字节)。
# 2.5.3 功能
在key对应的Bitmap中,查找指定字节范围内第一个值为value的bit位的偏移量。若未找到,返回-1。
# 2.5.4 返回值
第一个符合条件的bit位偏移量,未找到则返回-1。
# 2.5.5 示例
# 查找2026-01-13第一个签到的用户(第一个值为1的bit位)
127.0.0.1:6379> BITPOS sign:20260113 1
(integer) 5 # 用户ID为5的用户是当天第一个签到的
# 查找2026-01-13中,字节10-20(bit 80-160)范围内第一个未签到的用户(值为0的bit位)
127.0.0.1:6379> BITPOS sign:20260113 0 10 20
(integer) 83 # 用户ID为83的用户未签到,且是该范围内第一个未签到的
2
3
4
5
6
7
# 三、典型使用场景
Bitmap的空间效率和高效位运算特性,使其在多个场景中具有不可替代的优势。以下是最常见的应用场景:
# 3.1 用户签到统计
# 3.1.1 场景需求
记录用户每天的签到状态,支持查询单个用户某天是否签到、统计用户连续签到天数、统计某一天的总签到人数、统计某段时间内的签到次数等。
# 3.1.2 实现方案
- 键名设计:以“sign:日期”为键名(如sign:20260113),日期格式为年月日(YYYYMMDD)。
- 偏移量设计:以用户ID为偏移量(假设用户ID为整数)。
- 值设计:1表示已签到,0表示未签到。
# 3.1.3 核心操作
# 1. 用户ID 100 2026-01-13 签到
SETBIT sign:20260113 100 1
# 2. 查询用户ID 100 2026-01-13 是否签到
GETBIT sign:20260113 100
# 3. 统计2026-01-13 总签到人数
BITCOUNT sign:20260113
# 4. 统计用户ID 100 2026年1月(1-31日)的签到次数
# 遍历1-31日的键,执行GETBIT并累加1的个数
# 或使用BITOP OR合并31天的Bitmap,再统计1的个数(但需注意合并后无法区分具体天数)
# 5. 统计用户ID 100 连续签到天数(从当前日期往前推)
current_date = 20260113
continuous_days = 0
while True:
key = f"sign:{current_date}"
if GETBIT key 100 == 1:
continuous_days += 1
current_date = 前一天日期
else:
break
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.2 用户在线状态判断
# 3.2.1 场景需求
实时记录用户的在线/离线状态,支持快速查询单个用户状态、统计当前在线用户总数。
# 3.2.2 实现方案
- 键名设计:使用固定键名(如online:status),无需按日期拆分(因为状态是实时更新的)。
- 偏移量设计:以用户ID为偏移量。
- 值设计:1表示在线,0表示离线。
# 3.2.3 核心操作
# 1. 用户ID 200 上线,设置状态为1
SETBIT online:status 200 1
# 2. 用户ID 200 下线,设置状态为0
SETBIT online:status 200 0
# 3. 查询用户ID 200 是否在线
GETBIT online:status 200
# 4. 统计当前在线用户总数
BITCOUNT online:status
2
3
4
5
6
7
8
9
10
11
优势:相比使用Hash存储(如online:user {200:1, 201:0}),Bitmap节省90%以上的空间,且统计在线人数的效率更高(BITCOUNT vs HCOUNT)。
# 3.3 数据权限控制
# 3.3.1 场景需求
控制用户对多个资源的访问权限(如菜单、接口、文件),支持快速判断用户是否拥有某资源的权限,以及统计拥有某资源权限的用户数。
# 3.3.2 实现方案
- 键名设计:以“permission:资源ID”为键名(如permission:menu:101,表示菜单101的权限表)。
- 偏移量设计:以用户ID为偏移量。
- 值设计:1表示拥有权限,0表示无权限。
# 3.3.3 核心操作
# 1. 给用户ID 300 分配菜单101的权限
SETBIT permission:menu:101 300 1
# 2. 撤销用户ID 300 对菜单101的权限
SETBIT permission:menu:101 300 0
# 3. 判断用户ID 300 是否能访问菜单101
GETBIT permission:menu:101 300
# 4. 统计拥有菜单101权限的用户总数
BITCOUNT permission:menu:101
# 5. 统计同时拥有菜单101和菜单102权限的用户数(交集)
BITOP AND permission:menu:101_102 permission:menu:101 permission:menu:102
BITCOUNT permission:menu:101_102
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.4 布隆过滤器(Bloom Filter)底层实现
# 3.4.1 场景需求
快速判断一个元素是否存在于海量数据集合中(如判断用户是否为新用户、判断URL是否已爬取、判断订单号是否存在),允许一定的误判率,但要求极高的查询效率和空间效率。
# 3.4.2 实现方案
布隆过滤器的核心是多个哈希函数 + Bitmap:
- 将元素通过多个哈希函数计算得到多个整数偏移量。
- 将Bitmap中这些偏移量对应的bit位设置为1。
- 查询时,若所有偏移量对应的bit位均为1,则元素“可能存在”;若有一个为0,则元素“一定不存在”。
# 3.4.3 基于Redis Bitmap的实现示例
import hashlib
def hash_functions(element, num_hashes, bitmap_length):
"""生成num_hashes个哈希偏移量"""
offsets = []
for i in range(num_hashes):
# 结合i生成不同的哈希值
hash_val = hashlib.md5(f"{element}:{i}".encode()).hexdigest()
# 转换为整数偏移量,取模bitmap_length确保在范围内
offset = int(hash_val, 16) % bitmap_length
offsets.append(offset)
return offsets
# 初始化布隆过滤器(Bitmap长度为1000000bit,3个哈希函数)
BITMAP_KEY = "bloom:filter"
BITMAP_LENGTH = 1000000
NUM_HASHES = 3
# 1. 向布隆过滤器中添加元素(如用户ID:user_123456)
element = "user_123456"
offsets = hash_functions(element, NUM_HASHES, BITMAP_LENGTH)
for offset in offsets:
redis_client.setbit(BITMAP_KEY, offset, 1)
# 2. 查询元素是否存在
def is_exist(element):
offsets = hash_functions(element, NUM_HASHES, BITMAP_LENGTH)
for offset in offsets:
if redis_client.getbit(BITMAP_KEY, offset) == 0:
return False # 一定不存在
return True # 可能存在
print(is_exist("user_123456")) # True(可能存在)
print(is_exist("user_654321")) # False(一定不存在)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
优势:相比传统的集合(Set),布隆过滤器的空间效率极高(存储100万元素,误判率1%,仅需约1.2MB空间),查询时间复杂度为O(k)(k为哈希函数个数,通常为3-8)。
# 四、实现原理
# 4.1 底层存储:基于String的二进制安全特性
Redis的String类型底层存储的是字节序列(char[]),每个字节占8个bit位。Bitmap本质上是对这个字节序列的bit位进行直接操作。例如,当执行SETBIT key 10 1时,Redis会进行以下计算:
- 计算字节索引:offset / 8 = 10 / 8 = 1(即第2个字节,索引从0开始)。
- 计算bit位在字节中的位置:offset % 8 = 10 % 8 = 2(即该字节的第3个bit位,从0开始)。
- 读取该字节的原始值,通过位运算(OR)将对应bit位设为1,再写回字节序列。
若offset对应的字节不存在(即超过当前String的长度),Redis会自动扩展字节序列,填充0,直到字节索引对应的位置存在。
# 4.2 高效性保障:位运算的底层优化
Redis对位图操作的高效性,源于底层对CPU位运算指令的直接利用:
- SETBIT/GETBIT:单个bit位的操作仅需一次字节读取+位运算+字节写入,时间复杂度O(1)。
- BITCOUNT:采用了“查表法”优化。Redis预定义了一个256元素的数组,存储0-255每个整数的bit 1的个数(如0的个数为0,1的个数为1,2的个数为1,...,255的个数为8)。统计时,将Bitmap的每个字节作为索引查询数组,累加结果,时间复杂度O(n)(n为字节数),效率远高于逐bit位统计。
- BITOP:直接调用CPU的位运算指令(AND/OR/XOR/NOT),对多个字节序列进行并行运算,时间复杂度O(n)(n为最长字节序列的长度)。
# 4.3 内存占用计算
Bitmap的内存占用仅与最大偏移量有关,公式为: 内存占用(字节)= ceil(max_offset / 8)
例如:
- 最大偏移量为100(用户ID 100):ceil(100/8) = 13字节。
- 最大偏移量为1000万:ceil(10000000/8) = 1250000字节 ≈ 1.2MB。
- 最大偏移量为1亿:ceil(100000000/8) = 12500000字节 ≈ 12.5MB。
注意:若偏移量跳跃极大(如仅设置offset=10000000的bit位),Redis仍会占用1.2MB空间(即使其他bit位均为0),因为需要填充到最大偏移量对应的字节。因此,使用时应尽量保证偏移量连续或集中在较小范围内。
# 五、注意事项
# 5.1 偏移量的合理设计
- 避免过大的偏移量:若偏移量达到10亿,内存占用将达到125MB(10亿/8 = 125,000,000字节),若多个此类Bitmap并存,会占用大量内存。
- 偏移量映射:若数据标识为非整数(如用户名、UUID),需先建立映射表(如username_to_id),将非整数标识转换为连续的整数偏移量,否则无法发挥Bitmap的空间优势。
# 5.2 键的生命周期管理
Bitmap基于String类型,若不主动设置过期时间,会永久占用内存。对于时效性数据(如每日签到、实时在线状态),应通过EXPIRE命令设置合理的过期时间,避免内存泄漏。例如:
# 设置签到记录7天后过期(自动清理)
127.0.0.1:6379> EXPIRE sign:20260113 604800 # 604800秒 = 7天
2
# 5.3 并发操作的安全性
SETBIT命令是原子操作(Redis所有单键命令均为原子操作),无需担心并发修改冲突。例如,多个客户端同时对同一个offset执行SETBIT,Redis会保证操作的顺序性和原子性,最终结果正确。
但需注意:若业务逻辑需要“先查询再修改”(如先GETBIT判断状态,再SETBIT修改),则存在并发竞争问题,需通过Redis事务(MULTI/EXEC)或分布式锁保证原子性。例如:
# 确保用户只能签到一次(先查后改,需事务)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> GETBIT sign:20260113 100
QUEUED
127.0.0.1:6379> SETBIT sign:20260113 100 1
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 0 # 之前未签到
2) (integer) 0 # 设置成功
# 若之前已签到,GETBIT返回1,SETBIT仍会执行,但不影响结果(可在业务层判断是否需要执行SETBIT)
2
3
4
5
6
7
8
9
10
11
# 5.4 误判率问题(布隆过滤器场景)
基于Bitmap的布隆过滤器存在一定的误判率(即不存在的元素可能被判断为存在),误判率与Bitmap长度、哈希函数个数相关:
- Bitmap长度越长,误判率越低。
- 哈希函数个数越多,误判率越低,但查询效率会略有下降。
实际使用时,需根据业务允许的误判率(如1%、0.1%),通过公式计算合理的Bitmap长度和哈希函数个数,避免误判率过高影响业务。
# 5.5 不适合的场景
- 非布尔型数据存储:Bitmap仅能存储0/1,无法存储其他类型数据(如数值、字符串)。
- 非整数标识的数据:若数据标识无法转换为连续整数(如随机字符串),会导致偏移量过大,内存占用激增。
- 频繁更新和删除的场景:虽然SETBIT是原子操作,但Bitmap的删除(如删除某个用户的所有签到记录)需要遍历所有相关的Bitmap,效率较低(需通过键的过期时间间接清理)。
# 六、总结
Redis Bitmap是一种基于String类型的轻量级、高效数据结构,核心优势是极高的空间效率和高效的位运算能力。它通过将布尔型数据存储在bit位中,大幅降低了内存占用,同时支持SETBIT、GETBIT、BITCOUNT、BITOP等命令,满足签到统计、在线状态、权限控制、布隆过滤器等多种场景的需求。
使用Bitmap时,需注意合理设计偏移量、管理键的生命周期、规避并发问题,同时明确其适用场景(布尔型数据、整数标识),避免在不适合的场景中强行使用。在海量布尔型数据存储和统计场景中,Bitmap是优于Hash、Set等结构的最优选择之一。