ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

一文搞懂 Redis Bitmap:把它想成一座万人体育场的座位灯

2026/8/15 9:41:27 拓冰建站 浏览量
一文搞懂 Redis Bitmap:把它想成一座万人体育场的座位灯 文章目录一、Bitmap 到底是什么二、SETBIT检票员点亮一盏灯熄灯也很简单offset 从 0 开始而且按高位到低位排列三、GETBIT42 号观众到底来没来四、BITCOUNT今天到底亮了多少盏灯BITCOUNT 不是 O(1)范围默认按字节不是按 bit五、BITPOS第一盏亮灯在哪里六、BITOP把两天的座位灯板叠起来AND两天都来的人OR至少来过一天的人XOR两天状态不同的人DIFF昨天来了、今天没来的人BITOP 会把结果写进目标 KeyCluster 中要放在同一个 Slot七、BITFIELD一块灯板还能放多个小计数器溢出策略计数器爆表怎么办八、Bitmap 为什么省内存又为什么可能突然很大内存估算公式哪些 ID 不适合直接当 offset九、每天一块灯板TTL 怎么设计十、并发安全吗十一、Bitmap、Set、Sorted Set 怎么选十二、生产环境最常见的坑1. 把稀疏大 ID 直接当 offset2. 以为 Bitmap 是独立类型3. 忘记 BITCOUNT 范围默认按字节4. 高频扫描超大 Bitmap5. 在 Cluster 中让 BITOP 跨 Slot6. 需要列出所有用户却选了 Bitmap7. 每天一个 Key却忘记估算总量十三、完整 redis-cli 实验十四、快速判断口诀参考资料晚上七点五十九分万人体育场即将开场。检票口不断有人刷票入场运营大屏需要实时回答几个问题42 号观众进场了吗今天一共来了多少人昨天和今天都来的人有多少哪些人昨天来了、今天却没来四个入口分别进了多少人最直接的办法是准备一张名单把每个观众的 ID 存进去。但如果系统只关心“来过”或“没来过”给每个人保存一个完整数字就像为了记录座位是否有人给每把椅子配一名拿着纸笔的工作人员——能用但有点奢侈。体育场想到了一种更轻的办法每个座位上只装一盏灯。灯灭表示这个人没来灯亮表示这个人来了第 42 盏灯对应用户 42数一遍亮灯数量就是到场人数把两天的灯板叠起来还能算出连续到场、任意一天到场和流失观众。这块由 0 和 1 组成的座位灯板就是 Redis Bitmap。图 1一位观众对应一盏灯亮与灭分别表示两种状态。本文所有命令都在 Redis 8.6.1 的独立临时实例中验证。公开语义以 Redis 官方文档为准Redis 8.2 新增的位运算会单独标注版本。一、Bitmap 到底是什么先纠正一个常见说法Bitmap 并不是 Redis 新增的一种独立数据类型它本质上仍然是 String。Redis String 是二进制安全的字节序列。一个字节有 8 个 bitRedis 只是提供了一组命令让我们能把整段 String 当成一排开关来操作。座位编号 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 座位灯状态 0 0 1 0 0 1 0 0 | 0 0 0 0 1 0 0 0 字节编号 0 | 1现实世界和 Redis 可以这样对应体育场Redis Bitmap含义一整块座位灯板一个 String Key保存大量二值状态座位编号bit offset从 0 开始的位置灯亮bit 为 1已签到、在线、拥有权限等灯灭bit 为 0未签到、离线、无权限等检票员开灯SETBIT修改一个状态工作人员看灯GETBIT查询一个状态数亮灯BITCOUNT统计 1 的数量叠加两块灯板BITOP求交集、并集、差集图 2Bitmap 不是一排 Boolean 对象而是紧凑排列在 String 中的一串 bit。因为一个 bit 只有 0 和 1所以它特别适合记录二值状态用户今天是否登录学生今天是否签到设备这一小时是否上报商品是否参加活动用户是否拥有某项权限某天是否完成打卡。如果状态不是二选一例如还要保存进场时间、票价、入口和座位区域那么 Bitmap 就不能独自完成任务需要 Hash、Sorted Set 或数据库配合。二、SETBIT检票员点亮一盏灯假设 Keystadium:entry:2026-08-13表示 2026 年 8 月 13 日的进场灯板用户 ID 直接作为 offsetSETBIT stadium:entry:2026-08-1371SETBIT stadium:entry:2026-08-13421SETBIT stadium:entry:2026-08-1310011SETBIT key offset value的 value 只能是0或1。命令返回的不是新值而是修改前的旧值。127.0.0.1:6404 SETBIT stadium:entry:2026-08-13 42 1 (integer) 0 127.0.0.1:6404 SETBIT stadium:entry:2026-08-13 42 1 (integer) 1第一次返回 0说明原来灯是灭的这次是真正的首次签到第二次返回 1说明用户已经签到过。这个返回值很有用。比如业务要维护“今日签到人数”只有SETBIT返回 0 时才应该增加计数避免用户重复请求造成重复统计。不过要注意SETBIT bitmap userId 1 INCR daily_count这是两条命令不会自动组成一个原子动作。若必须同时更新 Bitmap 和计数器可以用 Lua 把“检查旧值、点灯、增加计数”放进一次执行中。相关原理可以参考博客里的 Redis 事务与 Lua 文章。熄灯也很简单观众退场或取消状态时把 bit 设回 0SETBIT stadium:entry:2026-08-13420offset 从 0 开始而且按高位到低位排列Redis 把第一个字节的最高有效位看作 offset 0然后向右依次编号offset 0 1 2 3 4 5 6 7 bit 权重 7 6 5 4 3 2 1 0因此SETBIT demo01得到的第一个字节是二进制10000000而不是00000001。平时使用用户 ID 作为 offset 时不太需要手算字节但在排查原始二进制数据时这个方向很容易让人看反。三、GETBIT42 号观众到底来没来查询某个用户是否签到GETBIT stadium:entry:2026-08-1342返回结果1查询一个没有被设置过的位置GETBIT stadium:entry:2026-08-1399返回0不存在的 Key 也会被当成全 0 的空字符串因此GETBIT不会因为 Key 不存在而报错。GETBIT和SETBIT都是 O(1)就像工作人员知道座位编号后直接看对应灯不需要从第一排一路找过去。图 3用户 ID 映射为 offset检票只修改一盏灯查询也只查看一盏灯。四、BITCOUNT今天到底亮了多少盏灯统计整块灯板中 1 的数量BITCOUNT stadium:entry:2026-08-13本文实验设置了 7、42、1001 三个位置返回3这就是今日签到人数。BITCOUNT 不是 O(1)BITCOUNT需要检查指定范围内的字节复杂度是 O(N)。体育场很小时数灯很快但一块几百 MB 的灯板反复全量统计仍会占用 Redis 主线程。工程上可以这样处理控制单个 Bitmap 的大小按天、小时或用户区间拆分不要在高峰期对超大 Bitmap 高频全量BITCOUNT对固定结果做短期缓存真正需要实时高频计数时维护额外计数器并用 Lua 保证更新原子性。范围默认按字节不是按 bitBITCOUNT key00默认统计的是第 0 个字节也就是 offset 07而不是只统计第 0 个 bit。Redis 7.0 起可以用BIT显式指定 bit 范围BITCOUNT key03BIT这才表示统计 offset 03。start和end都包含在范围内。本文用两个字节11110000 00001111验证BITCOUNT key - 8 BITCOUNT key 0 0 BYTE - 4 BITCOUNT key 0 3 BIT - 4看到0 0时千万不要条件反射地理解成“一个位置”先确认单位是 BYTE 还是 BIT。五、BITPOS第一盏亮灯在哪里BITPOS可以找到第一个值为 1 或 0 的 bitBITPOS stadium:entry:2026-08-131实验中最小的签到用户 ID 是 7所以返回7它适合寻找第一个已占用座位第一个空闲槽位数据流里第一次出现某种状态的位置。和BITCOUNT一样BITPOS最坏情况下需要扫描数据复杂度是 O(N)。范围参数默认也是按字节Redis 7.0 起可以追加BIT改为按 bit 解释。六、BITOP把两天的座位灯板叠起来Bitmap 最有意思的地方不只是省内存而是可以像叠透明灯板一样做位运算。假设两天的活跃用户如下8 月 12 日1、3、5、8 8 月 13 日3、5、6、8先建两块灯板SETBIT stadium:{active}:2026-08-1211SETBIT stadium:{active}:2026-08-1231SETBIT stadium:{active}:2026-08-1251SETBIT stadium:{active}:2026-08-1281SETBIT stadium:{active}:2026-08-1331SETBIT stadium:{active}:2026-08-1351SETBIT stadium:{active}:2026-08-1361SETBIT stadium:{active}:2026-08-1381AND两天都来的人BITOP AND stadium:{active}:both\stadium:{active}:2026-08-12\stadium:{active}:2026-08-13 BITCOUNT stadium:{active}:both结果是 3对应用户 3、5、8。OR至少来过一天的人BITOP OR stadium:{active}:either\stadium:{active}:2026-08-12\stadium:{active}:2026-08-13 BITCOUNT stadium:{active}:either结果是 5对应用户 1、3、5、6、8。XOR两天状态不同的人BITOP XOR stadium:{active}:changed\stadium:{active}:2026-08-12\stadium:{active}:2026-08-13结果中用户 1 和 6 为 1一个只在第一天出现一个只在第二天出现。DIFF昨天来了、今天没来的人Redis 8.2 为BITOP增加了DIFFBITOP DIFF stadium:{active}:lost\stadium:{active}:2026-08-12\stadium:{active}:2026-08-13 BITCOUNT stadium:{active}:lost结果是 1对应用户 1。Redis 8.2 同期还加入了DIFF1在后面的灯板中出现、但不在第一块灯板中ANDOR在第一块灯板中同时也在后续至少一块灯板中ONE在所有输入灯板中恰好只亮过一次。这些运算让留存、流失、独占人群分析更直接。但如果生产环境版本低于 8.2就不能使用这些新操作。图 4把两天的灯板重叠交集、并集和流失用户一眼就能算出来。BITOP 会把结果写进目标 KeyBITOP不是只返回临时结果而是覆盖写入destkey。目标 Key 要使用独立名称避免把原始日数据覆盖掉。BITOP复杂度是 O(N)N 与最长输入字符串有关。长度不同的输入会把短的部分视作 0结果长度等于最长输入。Cluster 中要放在同一个 SlotBITOP是多 Key 命令。在 Redis Cluster 中输入 Key 和目标 Key 必须属于同一个 Hash Slot。所以上面的 Key 都使用了相同 Hash Tagstadium:{active}:2026-08-12 stadium:{active}:2026-08-13 stadium:{active}:both花括号中的active相同这些 Key 才能落到同一 Slot。若直接使用不同槽位会收到CROSSSLOT。七、BITFIELD一块灯板还能放多个小计数器普通 Bitmap 把每个位置看成 1 bit只能表达开和关。BITFIELD则可以把连续多个 bit 解释成一个有符号或无符号整数。体育场有四个入口希望用一个 String 保存每个入口的客流数。每个入口分配 8 bit也就是一个u8无符号整数范围 0255| 入口 18 bit | 入口 28 bit | 入口 38 bit | 入口 48 bit |一次写入四个计数BITFIELD stadium:gates\SET u8#0 120 \SET u8#1 45 \SET u8#2 200 \SET u8#3 0#0表示第 0 个 u8 字段#1表示第 1 个 u8 字段比手算 offset 更直观。让入口 2 增加 5 人再读取前三个入口BITFIELD stadium:gates\INCRBY u8#1 5 \GET u8#0 \GET u8#1 \GET u8#2返回50 120 50 200第一项 50 是自增后的入口 2后面三项依次是入口 13。一个BITFIELD命令里的多个子操作按顺序执行整个命令由 Redis 串行处理。溢出策略计数器爆表怎么办u8 最大只能保存 255。BITFIELD提供三种溢出策略策略体育场类比行为WRAP计数器转一圈默认超过最大值从头绕回SAT指针顶在最大刻度钳制在最大或最小值FAIL拒绝继续登记返回 nil不修改字段BITFIELD stadium:gates OVERFLOW SAT INCRBY u8#2 100入口 3 原来是 200加 100 后不会变成 300而是停在 255。只读场景可以使用BITFIELD_RO让命令意图更明确。图 5普通 Bitmap 是一座一盏灯BITFIELD 则把若干连续灯位解释成小整数。八、Bitmap 为什么省内存又为什么可能突然很大如果要记录一亿个用户是否活跃理论上的原始位图大小是100,000,000 bit ÷ 8 ≈ 12.5 MB这通常比把一亿个整数成员放进 Set 紧凑得多。但 Bitmap 的空间取决于最大的 offset而不是亮灯数量。本文实验只设置 offset 1,000,000SETBIT stadium:sparse10000001STRLEN stadium:sparse即使只有一盏灯亮String 仍然增长到125001 bytes因为 Redis 必须把中间空缺位置全部补成 0。就像体育场只有第 100 万号座位有人你仍然得先修出前面所有座位。SETBIT的 offset 必须小于 (2^{32})所以单个 Bitmap 最多 512 MB。第一次直接设置极大的 offsetRedis 需要一次性分配并清零中间空间可能阻塞服务。官方文档特别提醒触碰接近上限的位置时要谨慎。内存估算公式忽略 Redis 对象头、分配器和碎片时字节数 ≈ floor(maxOffset / 8) 1实际值可用以下命令观察STRLEN stadium:sparse MEMORY USAGE stadium:sparseSTRLEN是 String 载荷字节数MEMORY USAGE还包含对象与内存分配开销因此通常更大。哪些 ID 不适合直接当 offsetUUID雪花 ID手机号跨业务拼接后的超大数字非常稀疏且跨度巨大的数据库主键。这些 ID 即使只有几个也可能把灯板撑得非常大。常见解决方案是为目标人群建立连续的小编号按用户区间分段例如每 100 万用户一个 KeyKey 里带日期或业务维度控制生命周期稀疏集合直接使用 Set不要为了用 Bitmap 而硬用 Bitmap。九、每天一块灯板TTL 怎么设计签到或日活常按天建 Keystadium:active:2026-08-13 stadium:active:2026-08-14写入后设置保留期限EXPIRE stadium:active:2026-08-137776000这里保留 90 天。TTL 作用于整个 String而不是某一个 bitRedis 没有“让第 42 个 bit 单独过期”的能力。如果每个用户的状态需要独立过期Bitmap 往往不是合适模型。可以考虑 Sorted Set 用时间戳做 Score或者将过期逻辑放到业务层。另外不要只在“Key 第一次创建时”凭感觉设置 TTL却没有检查异常路径。比较稳妥的做法是把 Key 命名、创建和过期策略封装在同一个业务入口并监控没有 TTL 的历史 Key。十、并发安全吗单条SETBIT、GETBIT、BITCOUNT和BITFIELD命令由 Redis 原子执行不会出现两个客户端把同一个字节写坏的问题。但“多条命令组成的业务动作”仍可能有竞态SETBIT 返回旧值 0 ↓ 应用准备 INCR 计数器 ↓ 应用在 INCR 前崩溃最后 Bitmap 已经亮灯计数器却少了一人。Redis 的单命令原子性不等于整个业务流程原子性。解决办法取决于要求只需要最终统计直接对 Bitmap 做BITCOUNTBitmap 与计数器必须一起改使用 Lua涉及数据库、消息队列和 Redis要做幂等、补偿或事件驱动不能只靠 Redis 单机事务幻想跨系统原子性。十一、Bitmap、Set、Sorted Set 怎么选需求更适合的结构原因连续整数 ID 的是否状态Bitmap每个成员只占 1 bit交并差快稀疏 ID、UUID、需要枚举成员Set成员独立保存模型更自然需要时间、分数、排名Sorted SetScore 可排序、可按范围查询只要近似去重数量HyperLogLog极省空间但不能判断具体成员每个对象有多个字段Hash能保存完整属性多个小整数紧凑存储BITFIELD可指定位宽并原子增减Bitmap 很像体育场的座位灯特别擅长回答“这个位置亮没亮”和“亮了多少盏”却不擅长保存观众姓名、票价和入场时间。十二、生产环境最常见的坑1. 把稀疏大 ID 直接当 offset只有一名用户也可能申请几百 MB。上线前一定要统计最大 ID、密度和增长速度。2. 以为 Bitmap 是独立类型TYPE stadium:entry:2026-08-13返回仍然是string它可以被普通 String 命令覆盖。若误执行SET key hello原位图就被替换了。3. 忘记 BITCOUNT 范围默认按字节BITCOUNT key 0 3默认是前 4 个字节即 32 个 bit想统计前 4 个 bit要写BITCOUNT key 0 3 BIT。4. 高频扫描超大 BitmapGETBIT是 O(1)但BITCOUNT、BITPOS和BITOP是 O(N)。不要看到“位图”两个字就默认所有操作都恒定时间。5. 在 Cluster 中让 BITOP 跨 Slot提前使用统一 Hash Tag并把目标 Key 也放进同一槽位。6. 需要列出所有用户却选了 BitmapBitmap 没有直接返回所有置位 offset 的高层命令。逐位扫描会很笨重如果核心需求是频繁枚举成员Set 往往更合适。7. 每天一个 Key却忘记估算总量一亿用户的一天原始位图约 12.5 MB保存 365 天大约 4.56 GB尚未计算副本、持久化、对象开销和内存碎片。单 Key 很省不代表乘以时间后仍然小。图 6先看 ID 是否连续再看最大 offset、命令复杂度、TTL 和 Cluster 槽位。十三、完整 redis-cli 实验下面这组命令可以直接复制执行。为了避免污染业务数据建议在测试 Redis 中使用独立前缀。# 1. 三名观众进场SETBIT lab:stadium:entry71SETBIT lab:stadium:entry421SETBIT lab:stadium:entry10011# 2. 查询与统计GETBIT lab:stadium:entry42GETBIT lab:stadium:entry99BITCOUNT lab:stadium:entry BITPOS lab:stadium:entry1STRLEN lab:stadium:entry MEMORY USAGE lab:stadium:entry# 3. 两天活跃用户SETBIT lab:stadium:{active}:day111SETBIT lab:stadium:{active}:day131SETBIT lab:stadium:{active}:day151SETBIT lab:stadium:{active}:day181SETBIT lab:stadium:{active}:day231SETBIT lab:stadium:{active}:day251SETBIT lab:stadium:{active}:day261SETBIT lab:stadium:{active}:day281# 4. 留存、累计、变化和流失BITOP AND lab:stadium:{active}:both\lab:stadium:{active}:day1 lab:stadium:{active}:day2 BITCOUNT lab:stadium:{active}:both BITOP OR lab:stadium:{active}:either\lab:stadium:{active}:day1 lab:stadium:{active}:day2 BITCOUNT lab:stadium:{active}:either BITOP XOR lab:stadium:{active}:changed\lab:stadium:{active}:day1 lab:stadium:{active}:day2 BITCOUNT lab:stadium:{active}:changed# Redis 8.2BITOP DIFF lab:stadium:{active}:lost\lab:stadium:{active}:day1 lab:stadium:{active}:day2 BITCOUNT lab:stadium:{active}:lost# 5. 四个入口的 u8 计数器BITFIELD lab:stadium:gates\SET u8#0 120 SET u8 #1 45 SET u8 #2 200 SET u8 #3 0BITFIELD lab:stadium:gates\INCRBY u8#1 5 GET u8 #0 GET u8 #1 GET u8 #2# 6. 溢出保护BITFIELD lab:stadium:gates OVERFLOW SAT INCRBY u8#2 100BITFIELD_RO lab:stadium:gates GET u8#2本文的隔离实验得到checked-in visitors: 3 first lit seat: 7 string bytes: 126 both days: 3 either day: 5 changed membership: 2 day1 but not day2: 1 offset 1,000,000 - string bytes: 125001 TYPE: string ENCODING: raw ALL ASSERTIONS PASSED这些数字是当前 Redis 8.6.1 实例的实测结果。MEMORY USAGE会受 Redis 版本、分配器和对象大小影响不应该当成所有机器通用的固定值。十四、快速判断口诀最后用一段体育场口诀收尾状态只有零和一ID 连续才适宜单点读写看座位人数统计数亮灯两块灯板做运算留存流失很清晰最大编号定空间稀疏大号要警惕单条命令虽原子跨步业务还得治Cluster 多 Key 同槽TTL 别忘按期清。Bitmap 的优势不是“任何场景都省内存”而是在状态简单、编号稠密、统计与集合运算频繁时用极紧凑的方式解决问题。选对场景它是一座高效的电子体育场选错场景它也可能只亮了一盏灯却先修出了几亿个空座位。参考资料Redis Bitmaps 官方文档SETBIT 命令GETBIT 命令BITCOUNT 命令BITPOS 命令BITOP 命令BITFIELD 命令Redis Cluster 规范个人小游戏