适用版本:Redis 7.2.x
一、什么是 Bitmap
Bitmap(位图)并不是 Redis 中一种独立的底层数据类型,它本质上是 基于 String 的按位操作能力。也就是说,Redis 会把一个字符串看作连续的二进制位数组,我们可以把每一位当成一个布尔状态位来使用:
0表示未发生1表示已发生
这种设计非常适合表示“某个对象是否具备某种状态”。例如:
- 用户今天是否签到
- 某个用户是否在线
- 某一天某个用户是否活跃
- 某个实验桶中的标记位是否被设置
如果用普通字符串、哈希或集合来保存这些状态,在数据量大时会非常占空间;而 Bitmap 只需要 1 bit 表示一个状态,因此空间效率极高。
二、Bitmap 的核心原理
1. 基于 String 存储位信息
Redis 的字符串底层是字节数组,而一个字节等于 8 位,所以一个字符串天然就可以被当成位数组。
例如,一个长度为 1 字节的字符串可以表示 8 个状态位:
字节内容:00000000
位偏移: 01234567
当我们执行:
SETBIT sign:20260609 0 1
SETBIT sign:20260609 3 1
可以理解为把第 0 位、第 3 位设置为 1。
2. 用 offset 表示下标
Bitmap 中最关键的概念是 位偏移量 offset。每个 offset 对应一个 bit 位。
比如:
- 用户 ID = 10086,可以把它映射到 offset = 10086
- 一个月签到场景中,第 1 天可以映射到 offset = 0,第 2 天映射到 offset = 1
这样就能用一个 key 存储成千上万甚至上亿个状态位。
3. 空间优势非常明显
假设要记录 1 亿个用户是否登录:
- 用 Bitmap:1 亿 bit ≈ 12 MB
- 用 Set 保存登录用户 ID:通常远大于 12 MB
这也是 Bitmap 在用户画像、活跃统计、签到系统中常见的原因。
三、Bitmap 适用场景
Bitmap 不是万能结构,它最适合解决“海量对象 + 二值状态”的问题。
1. 用户签到
例如记录 2026 年 6 月用户 1001 的签到情况:
- 第 1 天签到:offset 0 = 1
- 第 2 天未签到:offset 1 = 0
- 第 3 天签到:offset 2 = 1
可以进一步统计:
- 本月签到总天数
- 连续签到天数
- 今天是否签到
2. 日活/月活统计
可以为某一天建立一个 key:
active:20260609
把用户 ID 映射到偏移量,活跃过就置为 1。然后通过 BITCOUNT 统计这一天有多少用户活跃。
3. 用户标签或状态开关
例如:
- 是否完成新手引导
- 是否开启消息通知
- 是否参加活动
如果状态只有是/否两种,Bitmap 就非常合适。
4. 批量位运算分析
Redis 支持按位与、按位或、按位异或等操作,因此可以做:
- 连续两天都活跃的用户数
- 任意一天活跃的用户数
- 某两类标签的交集统计
四、常用命令与示例
下面结合具体例子来理解 Bitmap 的常用命令。
1. SETBIT:设置某一位
SETBIT sign:user:1001:202606 0 1
SETBIT sign:user:1001:202606 1 1
SETBIT sign:user:1001:202606 2 0
SETBIT sign:user:1001:202606 3 1
含义:
- 第 1 天签到
- 第 2 天签到
- 第 3 天没签到
- 第 4 天签到
返回值是该位设置前的旧值。
2. GETBIT:获取某一位
GETBIT sign:user:1001:202606 0
GETBIT sign:user:1001:202606 2
适合判断某一天是否已签到。
3. BITCOUNT:统计值为 1 的位数
BITCOUNT sign:user:1001:202606
如果结果为 3,表示这个月已经签到 3 天。
在 DAU 场景里,这个值就可以表示当日活跃人数。
4. BITOP:做多个 Bitmap 的位运算
统计两天都活跃的用户
BITOP AND active:and:20260608_20260609 active:20260608 active:20260609
BITCOUNT active:and:20260608_20260609
BITOP AND取交集BITCOUNT统计交集人数
统计两天任意一天活跃的用户
BITOP OR active:or:20260608_20260609 active:20260608 active:20260609
BITCOUNT active:or:20260608_20260609
5. BITPOS:查找第一个 0 或 1 出现的位置
BITPOS sign:user:1001:202606 1
BITPOS sign:user:1001:202606 0
这个命令有时可以辅助分析某些连续状态,但在签到系统里,更多还是结合应用层逻辑处理。
6. BITFIELD:按位段读写整数
BITFIELD user:flags SET u4 0 10 GET u4 0
这个命令可以把某一段 bit 当成整数读写,适合紧凑存储多个小整数标记。不过它比 SETBIT/GETBIT 更难理解,初学阶段先掌握基础命令即可。
五、一个典型案例:签到功能设计
1. Key 设计
假设要记录用户每月签到信息,可以设计成:
sign:{userId}:{yyyyMM}
例如:
sign:1001:202606
2. 写入签到
6 月 9 日签到,对应本月第 9 天,可以映射到 offset = 8:
SETBIT sign:1001:202606 8 1
3. 判断今天是否签到
GETBIT sign:1001:202606 8
4. 统计本月签到总天数
BITCOUNT sign:1001:202606
5. 连续签到如何做
连续签到通常不是 Redis 单条命令直接完成,而是:
- 取出相关 bit 信息
- 在业务代码中从当天开始向前遍历
- 遇到第一个 0 就停止
有些系统也会通过 Lua 脚本提高原子性和效率。
六、Bitmap 的优点
1. 极致节省空间
最大优点就是存储密度高。1 个状态只占 1 bit。
2. 统计效率高
Redis 对位操作做了优化,像 BITCOUNT、BITOP 这类命令在海量数据下依然很高效。
3. 适合布尔型状态建模
当业务问题能抽象成“是否”“有没有”“发生/未发生”时,Bitmap 往往是非常自然的选择。
七、使用 Bitmap 时的注意事项
1. Bitmap 适合二值状态,不适合复杂属性
如果你想保存的不只是“是否签到”,而是“签到时间、签到地点、签到来源”等复杂信息,Bitmap 就不够用了,需要配合 Hash、Stream、ZSet 等结构。
2. offset 过大可能造成空间浪费
Bitmap 的空间分配与 最大 offset 有关,而不是与设置了多少位有关。
例如你只设置:
SETBIT demo 100000000 1
那么 Redis 也需要为中间所有位扩容。因此:
- 用户 ID 稀疏、不连续时要谨慎
- 可以考虑建立用户 ID 到连续编号的映射
3. 大范围 BITOP 会带来额外开销
BITOP 会生成结果 key,如果参与运算的 Bitmap 很大,CPU 和内存都会有压力。
实践中要注意:
- 控制单次运算规模
- 对临时结果及时删除
- 避免在业务高峰期做超大范围离线统计
4. String 最大容量限制
Bitmap 基于 String,因此会受 String 最大容量限制。虽然上限已经很大,但在超大规模场景下仍应评估 key 的尺寸。
5. 不要把 Bitmap 当成“万能统计结构”
如果场景是去重计数,更适合 HyperLogLog;如果是地理位置检索,更适合 GEO;如果是消息队列,则更适合 Stream。数据结构选对了,系统才会简单。
八、实战设计建议
1. 优先明确“位偏移”的映射规则
Bitmap 的核心不是命令,而是映射关系。例如:
- 用户签到:offset = day - 1
- 用户活跃:offset = userId 映射值
- 小时活跃:offset = hour
映射规则一旦混乱,后续统计会很麻烦。
2. Key 命名中体现统计维度
建议把时间维度和对象维度放入 key 中,例如:
sign:user:{uid}:{yyyyMM}
active:day:{yyyyMMdd}
feature:flag:{name}
这样便于维护和清理。
3. 配合过期时间管理数据生命周期
例如日活、签到明细可能只保留近 3 个月,可以给 key 设置 TTL,减少长期堆积。
EXPIRE active:day:20260609 7776000
九、小结
Bitmap 是 Redis 中一个非常实用但又常被低估的能力。它不是独立数据类型,而是 String 的位级操作模型,最适合处理 海量数据下的二值状态统计。
可以重点记住以下几点:
- Bitmap 本质是对 String 做按位读写
- 一个状态只占 1 bit,空间利用率极高
- 适合签到、活跃统计、状态标记等场景
- 常用命令包括
SETBIT、GETBIT、BITCOUNT、BITOP - 需要特别注意 offset 设计和大范围位运算的成本
如果把 Redis 比作一个工具箱,那么 Bitmap 就像一把非常省空间的“状态记录器”。在“是否”问题上,它往往比 Set、Hash 更轻、更快、更划算。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!