Redis Bitmap(位图)
本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。
1. 定位:解决什么问题
Bitmap 是对 String 的位操作视图,用偏移量表达布尔状态。密集用户编号或连续日期适合位图;稀疏巨型 uid 用 Set 往往更合适。它不是 TYPE 返回的独立 bitmap 类型,也不能直接枚举原始业务 ID。
基本操作示例
在独立实验服务中使用教学键:
SETBIT learn:bitmap:daily:2026-10-20 42 1
GETBIT learn:bitmap:daily:2026-10-20 42
BITCOUNT learn:bitmap:daily:2026-10-20首次仅设置这一位时,读取结果与置位数量都为 1。重复设置同一位不会重复计数。
每日活跃与留存
按天建键,使用稳定的密集用户编号作为偏移量。例如 active:{zone1}:2026-10-20 表示该业务日的活跃集合。BITCOUNT 得到该天的精确活跃数;相同编号映射下,对两个日期做 BITOP AND 可得到两天都活跃的用户,再计数或除以首日人数计算留存。
若键仅包含月份,而偏移量仍是 uid,则统计的是整个月曾活跃的用户,不能直接得到 DAU。留存还需要明确分母、业务时区和统计口径。
连续签到
也可以每个用户每月一个键,偏移量使用“当月第几天减一”。此时 BITCOUNT 表示签到天数,不代表连续天数;连续签到要检查相邻日期,还要处理跨月、跨时区边界。
2. 命令速查与示例
按用途分组,共 7 项,覆盖 Redis 8.2.10 的本类型命令及子命令。点击命令名查看官方参数。示例在 redis-cli 中逐行输入,每次换例子前重置下面的教学数据。
准备与重置示例数据
在独立实验实例的 redis-cli 交互模式逐行输入,避免 shell 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。
UNLINK learn:{bitmap}:a learn:{bitmap}:b learn:{bitmap}:out
SET learn:{bitmap}:a A
SET learn:{bitmap}:b C示例的返回说明以刚完成准备段为前提,随机结果与剩余 TTL 不固定。带同一 {...} 的教学键便于在 Cluster 中满足相关多键操作的同槽要求。
位与位域操作
| 命令与用途 | 示例与结果 | 复杂度 |
|---|---|---|
| SETBIT 写入一位 | SETBIT learn:{bitmap}:a 2 1原来偏移 2 为 0,返回旧值 0。 | O(1) |
| GETBIT 读取一位 | GETBIT learn:{bitmap}:a 0A 的最高位为 0,返回 0。 | O(1) |
| BITCOUNT 统计置位数量 | BITCOUNT learn:{bitmap}:aA 的字节为 0x41,返回 2。 | O(B) |
| BITPOS 定位首个目标位 | BITPOS learn:{bitmap}:a 1A 的首个 1 位偏移为 1。 | O(B) |
| BITOP 对多个字符串做位运算 | BITOP AND learn:{bitmap}:out learn:{bitmap}:a learn:{bitmap}:bA 与 C 按位 AND 得到 A;覆盖 out,返回结果字节长度 1。 | O(B) |
| BITFIELD 读取、设置或增加位域 | BITFIELD learn:{bitmap}:a GET u8 0返回无符号字节值 65;SET/INCRBY 子操作需要另定溢出规则。 | 每个子操作 O(1) |
| BITFIELD_RO 只读位域 | BITFIELD_RO learn:{bitmap}:a GET u8 0返回 65,仅支持 GET 子操作。 | 每个子操作 O(1) |
版本与复杂度补充
下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。
| 命令 | 引入版本与状态 | 复杂度说明 |
|---|---|---|
| SETBIT | 2.2.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| GETBIT | 2.2.0 | O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。 |
| BITCOUNT | 2.6.0 | O(B),B 为扫描字节数。 |
| BITPOS | 2.8.7 | O(B),B 为扫描字节数。 |
| BITOP | 2.6.0 | O(B),B 随输入字节规模增长;结果分配与写入还有成本。 |
| BITFIELD | 3.2.0 | 每个子操作 O(1),多个子操作累加;首次大偏移扩容另计。 |
| BITFIELD_RO | 6.0.0 | 每个 GET 子操作 O(1),多个子操作累加;只读操作不会扩容字符串。 |
通用键操作见键与过期时间,这里集中列出类型自身的命令。
3. 底层编码与内存
位图底层仍为 String,通常是可修改的 raw/SDS 字节串,没有 bitmap→hashtable 的转换。容量主要取决于最高偏移,而不是置位数量。
没有位图专属 listpack 阈值。偏移 i 至少需要 floor(i/8)+1 字节有效载荷,再加字符串、键名和分配器开销;8.2 的 SETBIT 偏移范围需遵守命令限制,不能直接用无限增长的业务 ID。
编码与内存怎么测
在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:
SETBIT learn:bitmap:daily:2026-10-20 42 1
OBJECT ENCODING learn:bitmap:daily:2026-10-20
MEMORY USAGE learn:bitmap:daily:2026-10-20固定键名、元素字节长度、Redis 版本和分配器,分别记录 1、10、100 个元素或不同值长度时的内存,再跨越编码阈值观察。单元素的增量开销可用相邻两次内存差估计,编码转换时不能套用线性平均;String/Bitmap 的横轴应使用字节数或最高位偏移。比较“一键多元素”和“多个独立键”时,把键名、对象、字典项和 TTL 管理开销一起计入。
MEMORY USAGE 默认对嵌套值采样,SAMPLES 0 会遍历所有嵌套值,仅在小型隔离实验中使用。其结果不等同于进程 RSS;大量键还会增加全局字典、分配器碎片和复制缓冲等成本,需结合 INFO memory。没有适用于所有版本的固定“每 key 几字节”。
依据:8.2.10 配置、对应类型实现、MEMORY USAGE。
4. 性能与阻塞风险
高偏移的首次 SETBIT 需要扩容并初始化中间空间,即使命令标为 O(1) 也可能产生明显延迟。BITCOUNT 和 BITOP 扫描大字节串会占执行时间。没有 Bitmap SCAN;可按字节区间计数,但多次读取期间的数据可能变化。
常规命令执行主要占用主执行路径,耗时计算会拖慢同实例其他请求;网络 I/O、后台持久化和释放内存有其他线程/进程参与。命令复杂度不包含所有网络、分配、编码转换与客户端成本。
5. 原子性与并发
单次 SETBIT、BITOP 原子。先 GETBIT 再判断发奖会有竞态,可用 SETBIT 的旧值区分第一次置位;但置位与数据库发奖仍需幂等和失败恢复。多步留存计算要明确是否需要同一时刻的数据快照。
MULTI/EXEC 保证排队命令连续执行,但执行时单条报错不会回滚其他已成功命令;WATCH 是乐观冲突检测,需要有界重试。Lua 也不会自动回滚运行错误前的写入,应先校验参数与类型,再修改状态。
6. 过期、持久化与生命周期
只有整键 TTL,没有位级 TTL。按天建键管理 DAU 和留存,按用户按月建键管理签到;两种偏移含义不能混用。BITOP 目标覆盖会影响目标的生命周期,统计临时键应在运算后按流程设置有效期;清零高位不等于立刻缩小字符串分配。
大键会增加 RDB/AOF 重写、复制全量同步和恢复时的处理量,fork 写时复制还可能增加峰值内存。单次同步删除聚合对象可能阻塞;UNLINK 先摘除键,把主要回收工作移到后台,但总回收工作和内存压力仍存在,不能替代限长和拆分。
完成教学实验后,只清理自己创建的键,例如 UNLINK learn:bitmap:daily:2026-10-20。
7. 集群与扩展
多键 BITOP 需要同槽,例如同一区域的多个日期键使用共同 tag。按区域拆位图可分散内存,但跨区域留存须汇总且统一 ID 映射。副本滞后会让最新签到或 DAU 暂时偏低。
一个键只在一个分片中,hash tag 控制相关键进入同一槽,不会把一个大键切开。副本读取通常是最终一致观察;操作后立即读到本次写入的需求应选主节点或明确的协调策略。
8. Go 客户端与最佳实践
下面函数使用 github.com/redis/go-redis/v9,调用方提供 context.Context 和已配置认证、超时的 *redis.Client。
func BitmapExample(ctx context.Context, rdb *redis.Client, offset int64) (int64, error) {
if err := rdb.SetBit(ctx, "learn:go:daily", offset, 1).Err(); err != nil {
return 0, err
}
return rdb.BitCount(ctx, "learn:go:daily", nil).Result()
}Go 用整数 offset 和 0/1 表示位。调用前检查编号映射和最大偏移,避免把随机 64 位 uid 直接当偏移。SetBit 的 Result 返回旧位;需要“只算一次”时使用该结果而非先 GetBit。批量独立置位可分批 Pipeline。
普通 Pipeline 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。
继续阅读
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践