什么是 Redis Bitmap
在面试中,当面试官问到 Redis 的数据结构时,大多数人会脱口而出:String、List、Hash、Set、ZSet。但很少有人会主动提到 Bitmap。其实 Bitmap 并不是 Redis 独立的数据类型,它本质上是基于 String 类型实现的一种位操作能力。Redis 中的字符串在底层是二进制安全的字节数组,每个字节包含 8 个 bit,Bitmap 正是利用这些 bit 位来存储状态信息。
Redis 提供了一系列位操作命令,包括 SETBIT、GETBIT、BITCOUNT、BITOP、BITPOS、BITFIELD 等。通过这些命令,我们可以把每一个 bit 位当作一个独立的布尔值来使用,0 表示 false,1 表示 true。这种设计带来的最大优势就是极致的空间效率——一个 bit 只能存 0 或 1,但一亿个 bit 也只需要约 12MB 的内存。
核心命令速览
在展开业务场景之前,先快速回顾几个关键命令:
- SETBIT key offset value:设置指定偏移量上的 bit 值(0 或 1)
- GETBIT key offset:获取指定偏移量上的 bit 值
- BITCOUNT key [start end]:统计指定范围内 bit 值为 1 的数量
- BITOP operation destkey key [key …]:对一个或多个 Bitmap 做 AND、OR、XOR、NOT 操作
- BITPOS key bit [start end]:查找第一个指定 bit 值的位置
理解这些命令是掌握 Bitmap 应用场景的基础。
场景一:用户签到系统
这是 Bitmap 最经典的应用场景,也是面试中出现频率最高的。
假设我们需要记录每个用户每月的签到情况。如果使用传统的数据库方案,每次签到插入一条记录,一个月 30 天、100 万用户就是 3000 万条记录,存储和查询成本都不低。而使用 Bitmap,每个用户每月的签到数据只需要一个 key,比如 sign:user:1001:202406,offset 从 0 到 29 分别代表 6 月 1 日到 6 月 30 日。
用户签到时就执行 SETBIT sign:user:1001:202406 5 1,表示 6 月 6 日已签到。查询当月签到次数用 BITCOUNT sign:user:1001:202406,查询某天是否签到用 GETBIT,查询连续签到天数则可以通过 BITPOS 或遍历实现。整个用户一个月的签到数据只占 4 个字节(30 个 bit),100 万用户一个月也才约 4MB。
更进一步,可以通过 BITOP AND 计算多个用户的共同签到日期,或者通过 BITOP OR 合并多个 Bitmap 做全局统计。
场景二:活跃用户统计
产品运营经常需要统计日活(DAU)、周活(WAU)、月活(MAU)。传统做法是在用户访问时写入一条日志,然后通过 SQL 的 COUNT(DISTINCT user_id) 来统计,数据量大时性能堪忧。
用 Bitmap 的思路是:以日期为 key,用户 ID 作为 offset。比如 active:20240606 这个 key,用户 1001 访问了就执行 SETBIT active:20240606 1001 1。统计日活直接 BITCOUNT active:20240606,一条命令搞定。
统计周活时,用 BITOP OR weekly:2024W23 active:20240603 active:20240604 ... active:20240609,然后对结果做 BITCOUNT。月活同理。这种方式不仅快,而且多个 Bitmap 做 OR 操作的复杂度是 O(N),N 是 Bitmap 的字节长度,效率非常高。
需要注意的是,用户 ID 必须是连续的整数才能作为 offset 使用。如果用户 ID 是雪花算法生成的 64 位长整型,offset 会非常大,导致 Bitmap 占用过多内存。这时候需要做一层映射,或者使用 Redis 的 HyperLogLog 作为替代方案(但 HyperLogLog 有误差,且不支持判断单个用户是否活跃)。
场景三:布隆过滤器
布隆过滤器本质上就是一个 Bitmap 加上多个哈希函数。虽然 Redis 有专门的 RedisBloom 模块提供布隆过滤器,但在没有该模块的情况下,我们完全可以基于 Bitmap 手动实现一个简单的布隆过滤器。
典型应用是缓存穿透防护:当查询一个不存在的 key 时,请求会直接打到数据库。我们可以在缓存层前加一个布隆过滤器,将所有存在的 key 预先存入。查询时先经过布隆过滤器判断,如果判断不存在则直接返回,避免无效的数据库查询。
实现方式是:选择 k 个哈希函数,对每个 key 计算出 k 个 offset,将这些位置的 bit 置为 1。查询时同样计算 k 个 offset,如果所有位置都是 1,则可能存在(注意布隆过滤器有假阳性);如果有任何一个位置是 0,则一定不存在。
场景四:用户画像标签
在推荐系统和广告投放中,经常需要给用户打标签,比如“是否购买过某品类”“是否点击过某广告”。如果每个标签用一个 Set 存储用户 ID,当标签数量多、用户基数大时,内存消耗会非常可观。
用 Bitmap 可以这样设计:每个标签对应一个 Bitmap,用户 ID 作为 offset。判断用户是否拥有某个标签用 GETBIT,统计拥有某标签的用户数用 BITCOUNT,找同时拥有多个标签的用户用 BITOP AND。
比如要找到“既购买过电子产品又购买过图书”的用户群体,只需要对两个 Bitmap 做 AND 操作,然后 BITCOUNT 得到人数,或者遍历结果 Bitmap 获取具体用户列表。这种集合运算在 Set 上做交集的时间复杂度是 O(N),而 Bitmap 的 BITOP 是位运算,在数据密集的场景下效率更高。
场景五:IP 地址黑名单与访问控制
对于需要记录大量布尔状态的场景,Bitmap 都非常适合。比如记录某 IP 段内哪些 IP 被拉黑,可以将 IP 地址转换为整数作为 offset。又比如在限流场景中,记录某个用户在过去 60 秒内哪些秒有请求,用于滑动窗口限流。
再举一个实际例子:统计某篇文章每一天是否被用户阅读过。以文章 ID 为 key,日期为 offset,用户阅读后就 SETBIT。这样可以快速知道文章在哪些天有阅读量,也可以通过 BITCOUNT 统计总阅读天数。
使用 Bitmap 的注意事项
虽然 Bitmap 空间效率极高,但也不是银弹,使用时需要注意几个问题。
offset 不宜过大。Redis 的 Bitmap 是按需分配的,但如果 offset 设置为 10 亿,Redis 会直接分配 10 亿个 bit(约 125MB)的内存,即使只设置了一个 bit。因此 offset 必须尽可能紧凑,用户 ID 最好做映射或者使用自增 ID。
BITCOUNT 的时间复杂度是 O(N),N 是 Bitmap 的字节长度。对于超大的 Bitmap,BITCOUNT 可能成为性能瓶颈。可以通过分片的方式,将大 Bitmap 拆分为多个小 Bitmap。
Bitmap 不适合稀疏数据。如果一亿个 offset 中只有几百个被设置为 1,使用 Bitmap 反而浪费内存,这时候用 Set 更合适。Bitmap 的优势在于数据密集、状态只有两种的场景。
BITOP 操作的结果 key 会占用内存,如果频繁做 BITOP 且不及时清理,可能导致内存膨胀。
面试答题思路
如果在面试中被问到 Bitmap 的使用场景,建议按以下结构回答:
第一,先解释 Bitmap 的本质——基于 String 的位操作,核心优势是省内存和位运算高效。
第二,列举两到三个典型场景,签到和活跃用户统计是必答的,能体现你对业务的理解。
第三,主动提到注意事项,比如 offset 过大的内存问题、稀疏数据不适合用 Bitmap,这能体现你的思考深度。
第四,如果能对比其他方案(如 Set、HyperLogLog)的优劣,说明你在实际选型时有权衡意识,会给面试官留下更好的印象。
Bitmap 是 Redis 中一个看似简单却非常实用的工具,掌握它的原理和适用边界,不仅能在面试中加分,在实际工作中也能帮你设计出更优雅的解决方案。
未经允许不得转载:任鹏个人博客 » Redis Bitmap 在实际业务中的使用场景

