引言
在前面的 Redis 系列中,我们学习了五种基本数据类型和发布订阅。今天要讲的位图(Bitmap) 不是一种独立的数据类型,而是 String 类型的一种特殊用法------把字符串当成二进制位数组来操作。
位图的核心思想是:用一个 bit 表示一个状态(0 或 1)。1 字节 = 8 个 bit,可以表示 8 个独立的状态。这种极致的内存效率,让位图特别适合海量数据的布尔统计场景------用户签到、活跃用户统计、权限判断等。

第一部分:位图的基本原理
一、位图是什么
位图本质就是一个 String 字符串 ,但操作的单位不是字节,而是比特(bit)。Redis 提供了专门操作比特位的命令。

关键概念:
| 概念 | 说明 |
|---|---|
| offset | 位的偏移量,从 0 开始(从左向右计数) |
| 最大值 | 2³² - 1 = 约 42 亿 bit ≈ 512 MB |
| 底层类型 | String,自动扩容 |
二、为什么用位图
| 场景 | 传统做法 | 位图做法 | 节省 |
|---|---|---|---|
| 1 亿用户签到 | 1 亿个 key | 约 12MB 一个 key | 数千倍 |
| 日活统计 | set/hash | 约 12MB | 数百倍 |
| 权限 100 项 | hash 存储 | 约 13 字节 | 数十倍 |
第二部分:核心命令
一、设置位:SETBIT
bash
# 语法:SETBIT key offset value
# value 只能是 0 或 1
127.0.0.1:6379> SETBIT user:1:sign 0 1 # 第 0 天签到
(integer) 0 # 返回旧值
127.0.0.1:6379> SETBIT user:1:sign 1 1 # 第 1 天签到
(integer) 0
127.0.0.1:6379> SETBIT user:1:sign 5 1 # 第 5 天签到
(integer) 0
127.0.0.1:6379> SETBIT user:1:sign 0 0 # 取消第 0 天签到
(integer) 1 # 返回旧值为 1
注意:
-
offset 可以从 0 到 2³²-1(约 42 亿)
-
Redis 会自动扩容字符串以容纳更大的 offset
-
返回值是设置前的旧值
二、获取位:GETBIT
bash
# 语法:GETBIT key offset
127.0.0.1:6379> GETBIT user:1:sign 0
(integer) 0
127.0.0.1:6379> GETBIT user:1:sign 1
(integer) 1
127.0.0.1:6379> GETBIT user:1:sign 2 # 未设置过
(integer) 0 # 默认返回 0
三、统计位数:BITCOUNT
bash
# 语法:BITCOUNT key [start end]
# 统计有多少个 1
127.0.0.1:6379> BITCOUNT user:1:sign
(integer) 2 # 有两个 1
# 统计指定字节范围(注意是字节,不是 bit!)
127.0.0.1:6379> BITCOUNT user:1:sign 0 0 # 第 0 个字节
(integer) 1
BITCOUNT 的 start/end 是字节索引,不是 bit 索引:
BITCOUNT key 0 0 → 只统计第 0 个字节(bit 0~7)
BITCOUNT key 0 1 → 统计第 0~1 个字节(bit 0~15)
BITCOUNT key 0 -1 → 统计所有字节
四、查找位:BITPOS
bash
# 语法:BITPOS key bit [start] [end]
# 查找第一个值为 bit 的位置
127.0.0.1:6379> BITPOS user:1:sign 1 # 第一个 1 的位置
(integer) 1
127.0.0.1:6379> BITPOS user:1:sign 0 # 第一个 0 的位置
(integer) 0
# 在指定字节范围内查找
127.0.0.1:6379> BITPOS user:1:sign 1 1 5 # 第 1~5 字节内找第一个 1
五、位运算:BITOP
bash
# 语法:BITOP operation destkey key [key ...]
# 支持:AND(与)、OR(或)、XOR(异或)、NOT(非)
127.0.0.1:6379> SETBIT user:1:day1 0 1
127.0.0.1:6379> SETBIT user:1:day2 0 1
127.0.0.1:6379> SETBIT user:2:day1 0 1
# AND:两天都签到的用户
127.0.0.1:6379> BITOP AND both_days user:1:day1 user:1:day2
(integer) 1
# OR:任意一天签到的用户
127.0.0.1:6379> BITOP OR any_day user:1:day1 user:1:day2
(integer) 1
# XOR:恰好只签了一天的用户
127.0.0.1:6379> BITOP XOR only_one user:1:day1 user:1:day2
(integer) 1
位运算的应用:
| 运算 | 场景 |
|---|---|
AND |
连续 N 天都签到的用户 |
OR |
N 天内任意一天签到的用户 |
XOR |
恰好签了奇数天的用户 |
NOT |
没有签到的用户 |
第三部分:实战场景
一、用户签到系统
bash
# 用户 1001 在第 0 天签到
127.0.0.1:6379> SETBIT sign:2024:06:01 1001 1
(integer) 0
# 用户 1002 签到
127.0.0.1:6379> SETBIT sign:2024:06:01 1002 1
(integer) 0
# 查看用户 1001 是否签到
127.0.0.1:6379> GETBIT sign:2024:06:01 1001
(integer) 1
# 统计今日签到人数
127.0.0.1:6379> BITCOUNT sign:2024:06:01
(integer) 2
# 统计本月全勤用户(6月1日~30日都签到)
127.0.0.1:6379> BITOP AND full_attendance sign:2024:06:01 sign:2024:06:02 ... sign:2024:06:30
127.0.0.1:6379> BITCOUNT full_attendance
二、统计在线活跃用户
bash
# 记录每个用户的活跃状态(user_id 作为 offset)
# 2024-06-01 活跃用户
127.0.0.1:6379> SETBIT active:2024-06-01 1001 1
127.0.0.1:6379> SETBIT active:2024-06-01 1002 1
127.0.0.1:6379> SETBIT active:2024-06-01 1005 1
# 2024-06-02 活跃用户
127.0.0.1:6379> SETBIT active:2024-06-02 1001 1
127.0.0.1:6379> SETBIT active:2024-06-02 1003 1
# 两天都活跃的用户
127.0.0.1:6379> BITOP AND active_both active:2024-06-01 active:2024-06-02
127.0.0.1:6379> BITCOUNT active_both
(integer) 1 # 只有用户 1001 两天都活跃
三、权限管理
bash
# 100 个权限位,每位代表一个权限
# bit 0: 读权限 bit 1: 写权限 bit 2: 删除权限 bit 3: 管理权限 ...
# 管理员拥有所有权限(前 10 位都设为 1)
127.0.0.1:6379> SETBIT user:admin:permissions 0 1
127.0.0.1:6379> SETBIT user:admin:permissions 1 1
...
127.0.0.1:6379> SETBIT user:admin:permissions 9 1
# 普通用户只有读权限
127.0.0.1:6379> SETBIT user:normal:permissions 0 1
# 检查权限
127.0.0.1:6379> GETBIT user:normal:permissions 1
(integer) 0 # 没有写权限
第四部分:C 语言操作位图
cpp
#include <stdio.h>
#include <hiredis/hiredis.h>
int main() {
redisContext *c = redisConnect("127.0.0.1", 6379);
if (c == NULL || c->err) {
printf("连接失败: %s\n", c->errstr);
return -1;
}
// 设置签到
redisReply *reply = redisCommand(c, "SETBIT sign:2024:06:01 1001 1");
printf("设置结果(旧值): %lld\n", reply->integer);
freeReplyObject(reply);
// 检查是否签到
reply = redisCommand(c, "GETBIT sign:2024:06:01 1001");
printf("签到状态: %lld\n", reply->integer);
freeReplyObject(reply);
// 统计签到人数
reply = redisCommand(c, "BITCOUNT sign:2024:06:01");
printf("签到人数: %lld\n", reply->integer);
freeReplyObject(reply);
redisFree(c);
return 0;
}
第五部分:位图的局限性
| 局限 | 说明 | 解决方案 |
|---|---|---|
| offset 映射 | user_id 需要是连续整数 | 维护 user_id → 整数映射表 |
| 稀疏数据浪费 | 如果 offset 很大但只有几个 1 | 用集合或布隆过滤器 |
| 不可直接遍历 1 | BITCOUNT 只统计数量 | BITPOS 逐个查找 |
| 字符串最大 512MB | offset 最大约 42 亿 | 分片存储 |
总结
一、核心命令速查
| 命令 | 作用 |
|---|---|
SETBIT key offset 0/1 |
设置某位的值 |
GETBIT key offset |
获取某位的值 |
BITCOUNT key [start end] |
统计 1 的数量(字节范围) |
BITPOS key bit [start end] |
查找第一个 bit 的位置 |
BITOP AND/OR/XOR/NOT dest key1 key2 |
位运算 |
二、一句话记忆
Redis 位图用 String 类型的每个 bit 表示一个布尔状态,1 bit 存一个信息。用 SETBIT/GETBIT 操作单个位,用 BITCOUNT 统计数量,用 BITOP 做集合运算。适合签到、活跃用户、权限等海量布尔统计场景,空间效率是普通 key-value 的数百倍。