Bitmap 很容易给人一种「按位存储,所以一定省内存」的感觉。这个判断只说对了一半:当 offset 连续且密集时它确实省;当 offset 稀疏又巨大时,它会直接按最大 offset 扩容。
SETBIT key 100000000 1 不会只存一个 1。Redis 要把底层 String 扩到能覆盖这个位置,中间的字节全部清零填上。这就是某些线上事故里,一条看起来无害的命令瞬间吃掉十几 MB 内存的原因。
先把机制边界说清楚
这一篇讨论 Bitmap 和 Bitfield 的内存模型,不讨论所有统计系统。Bitmap 适合布尔标记和密集 ID 场景,不适合把雪花 ID、手机号这类大跨度稀疏值直接当 offset。一旦你想存「某用户是否做过某事」而用户 id 是 64 位随机串,Bitmap 就已经选错了。
整体路径
上面这张图先把主线铺开:最大 offset 决定 SDS 至少要扩到哪里。所以 Bitmap 的代价主要看 offset 上限和稀疏 id 造成的扩容,而不是命令名。
Bitmap 没有底层类型,它就是 String 的位寻址
Redis 里不存在一个叫「bitmap」的独立数据结构。SETBIT、GETBIT 操作的全是 String,底层就是 SDS 的字节数组。所谓 Bitmap,只是 String 暴露出来的位级接口。
机制很简单:SETBIT key offset 1,Redis 把 offset 换算成「第几个字节的第几位」。字节序号 = offset / 8。如果这个字节序号超过了当前 SDS 长度,就触发扩容——扩到刚好覆盖它,再把中间所有新增字节清零。扩容和清零都在主线程上做,是同步的。
算笔账:offset = 1 亿,对应字节 1250 万,即 12.5 MB。一个 SETBIT key 100000000 1 瞬间吃掉 12MB,不是因为你存了一亿个用户,而是因为你存的这一个用户的 id 太大。STRLEN 看到的就是这 12MB,MEMORY USAGE 还要叠加 SDS 头和 jemalloc 的分配对齐开销。
边界风险也在这里:SETBIT 的 offset 上限是 2^32-1,单位是 bit,不是 byte;因此单个 Bitmap 最大字符串约 512 MiB。有人手滑把一个不该当 offset 的大整数(比如时间戳、雪花 id)塞进去,一条命令就能把实例打爆。所以 Bitmap 业务的铁律是:上线前估算最大 offset,而不是估算置 1 的数量。
签到与活跃统计:Bitmap 的舒适区
Bitmap 真正香的地方是连续整数 id + 布尔标记。最典型的是签到和日活。
签到 key 设计:type:sign:{uid}:{yyyymm},一个用户一个月一个 key,每天一个 bit。uid 必须是连续小整数(比如业务自增 id)。一个用户一个月只占 4 字节(31 位),GETBIT 取某天、BITCOUNT 取月签到天数,都极快。
日活和留存靠组合位运算:当天活跃用户存成 dau:{yyyymmdd},BITCOUNT 就是当天 DAU。算次日留存:BITOP AND result dau:20260622 dau:20260623,再 BITCOUNT result,得到两天都活跃的用户数。算七日并集活跃:BITOP OR。
这套玩法的隐含前提是:用户 id 必须能直接当 offset,也就是「连续、稠密、小」。一旦 id 稀疏(比如真实 uid 跨度到几亿但活跃只有几万),Bitmap 的 dau:{date} 就会虚胖——最大 id 决定占用,而不是活跃人数决定。这时要么先做一层「uid → 连续序号」的映射,要么换方案。
去重三选一:Bitmap / Set / HyperLogLog
同样是「统计独立用户数」,三种方案的取舍很清晰,关键看你要不要精确、要不要成员、id 稠不稠密:
- Bitmap:稠密连续 id 下最省,支持精确计数和成员判断(
GETBIT),还能做位运算算交集并集。代价是依赖 id 稠密,稀疏时浪费。 - Set:精确,能
SISMEMBER查成员,能做交集并集。代价是每个成员都要存原值,UV 上千万时内存会很重,通常是三者里最贵的。 - HyperLogLog:dense 表示最坏约 12KB,低基数 sparse 表示会更小。代价是只有估算值(误差约 0.81%),拿不到成员、不能查是否出现过、不能精确交集。
判断很简单:要精确成员用 Set,要精确计数且 id 稠密用 Bitmap,只要一个大概数字且量大用 HyperLogLog。三者经常配合:HLL 算大盘 UV,Bitmap 做留存漏斗,Set 只在必须查成员的小集合上用。
典型问题:用机制化例子排查
- 上线前估算最大 offset,不是置 1 数量;offset 跳变会按最大值扩容。
- 稀疏 id 先做连续映射或按日期/业务分桶,别让一个 key 扛全部。
- 用
STRLEN和MEMORY USAGE看真实占用,别只数 bit。 - 大范围
BITOP放低峰期,几个千万位 Bitmap 做 AND/OR 也会扫主线程。 - 留存、漏斗用 Bitmap,大盘 UV 用 HLL,必须查成员才上 Set。
收束:一句判断
Bitmap 省的是密集布尔集合,不是任意稀疏空间。它好不好用,取决于你的 id 稠不稠密。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 Redis」,欢迎关注公众号 「十三Tech」。后续会继续按数据结构、底层机制、持久化、高可用和实战排查这条线更新。

