Skip to content
← 返回Redis列表
数据与原子性

Redis Bitmap(位图) ​

本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。

1. 定位:解决什么问题 ​

Bitmap 是对 String 的位操作视图,用偏移量表达布尔状态。密集用户编号或连续日期适合位图;稀疏巨型 uid 用 Set 往往更合适。它不是 TYPE 返回的独立 bitmap 类型,也不能直接枚举原始业务 ID。

基本操作示例 ​

在独立实验服务中使用教学键:

text
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 对引号、>、*、括号等符号进行解释。每个例子独立使用;换例子前重新执行下面的准备段,否则删除、弹出、过期和修改操作会影响后续结果。准备段仅重置列出的教学键,确认这些键不承载业务数据。

text
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 0
A 的最高位为 0,返回 0。
O(1)
BITCOUNT
统计置位数量
BITCOUNT learn:{bitmap}:a
A 的字节为 0x41,返回 2。
O(B)
BITPOS
定位首个目标位
BITPOS learn:{bitmap}:a 1
A 的首个 1 位偏移为 1。
O(B)
BITOP
对多个字符串做位运算
BITOP AND learn:{bitmap}:out learn:{bitmap}:a learn:{bitmap}:b
A 与 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)
版本与复杂度补充

下表保留每条命令的引入版本、兼容状态及复杂度符号含义;复杂度不包含全部网络传输、内存分配和等待成本。

命令引入版本与状态复杂度说明
SETBIT2.2.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
GETBIT2.2.0O(1)。大值复制、扩容、网络输出和等待时间需另外考虑。
BITCOUNT2.6.0O(B),B 为扫描字节数。
BITPOS2.8.7O(B),B 为扫描字节数。
BITOP2.6.0O(B),B 随输入字节规模增长;结果分配与写入还有成本。
BITFIELD3.2.0每个子操作 O(1),多个子操作累加;首次大偏移扩容另计。
BITFIELD_RO6.0.0每个 GET 子操作 O(1),多个子操作累加;只读操作不会扩容字符串。

通用键操作见键与过期时间,这里集中列出类型自身的命令。

3. 底层编码与内存 ​

位图底层仍为 String,通常是可修改的 raw/SDS 字节串,没有 bitmap→hashtable 的转换。容量主要取决于最高偏移,而不是置位数量。

没有位图专属 listpack 阈值。偏移 i 至少需要 floor(i/8)+1 字节有效载荷,再加字符串、键名和分配器开销;8.2 的 SETBIT 偏移范围需遵守命令限制,不能直接用无限增长的业务 ID。

编码与内存怎么测 ​

在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:

text
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。

go
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 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。

继续阅读 ​

数据类型总览 · 游戏与通用后端实践 · 原子性基础。

参考:Redis 8.2.10 命令定义、go-redis、集群规格。

继续阅读