上一篇【第59篇】SORT命令——Redis里的万能排序指令下一篇【第61篇】 慢查询日志——找出Redis性能瓶颈的利器明日更新敬请期待如果有人告诉你1亿个用户的活跃状态只需要12.5MB就能存下你信不信一年的用户签到记录只需要46字节你信不信这不是魔法这是Redis的BitMap——用二进制位来存储和操作数据的一种方式。BitMap不是Redis的新数据类型而是String类型的一种特殊视角——把String当作一个由0和1组成的数组来使用。听起来很底层对吧但恰恰是这种底层操作带来了极高的空间效率和计算效率。BitMap本质——String类型的二进制视图BitMap不是一个独立的数据类型它是String类型的位级操作接口。一个String键最多可以存储512MB的数据也就是 512 × 1024 × 1024 × 8 4,294,967,296个位约43亿位。String → BitMap 视角转换 String视图: A ASCII码: 65 01000001 BitMap视图: ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ └───┴───┴───┴───┴───┴───┴───┴───┘ 位7 位6 位5 位4 位3 位2 位1 位0 你可以用SETBIT/GETBIT操作其中的每一位关键理解BitMap就是StringString就是BitMap。只不过我们用位操作的视角来看它。你可以用GET命令查看BitMap的完整内容也可以用SET命令直接设置整个BitMap——但通常我们用位操作命令。位数组存储布局理解BitMap的存储布局非常重要因为位的编号方式和直觉可能不同BitMap 存储布局 字节偏移: 字节0 字节1 字节2 ┌─────────────┬─────────────┬─────────────┐ 位偏移: │0 1 2 3 4 5 │6 7 8 9 ... │... │ │6 7 4 5 2 3 │0 1 2 3 │ │ └─────────────┴─────────────┴─────────────┘ 更详细地看一个字节: 字节0: ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ 位7 │ 位6 │ 位5 │ 位4 │ 位3 │ 位2 │ 位1 │ 位0 │ │ 128 │ 64 │ 32 │ 16 │ 8 │ 4 │ 2 │ 1 │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ 高位 低位 规则: • 字节从左到右排列字节0, 字节1, 字节2, ... • 位从高到低排列位7是最高位位0是最低位 • 位偏移 字节偏移 × 8 位内偏移 • SETBIT key 10 1 → 字节1的第1位从高位数第6位⚠️ 注意SETBIT和GETBIT的offset参数是位偏移量从0开始。offset0是第一个字节最高位offset7是第一个字节最低位offset8是第二个字节最高位以此类推。这个编号方式和网络字节序大端序一致。稀疏存储特性BitMap的一个重要特性未设置的位置自动为0不需要显式初始化。# 直接设置第1000万位SETBIT mymap100000001# Redis会自动在前面填充0只分配必要的内存# 查看第0位未设置自动为0GETBIT mymap0# → (integer) 0但这也意味着如果你SETBIT一个很大的offsetRedis会分配大量内存# 这条命令会分配约 1.25MB 的内存SETBIT mymap100000001# 因为 Redis 需要创建从0到10000000的所有字节⚠️ 注意避免SETBIT一个超大的offset如2^32-1这会导致Redis分配512MB的内存。攻击者可能利用这一点进行DoS攻击——在开放Redis端口的公网环境中尤其危险。SETBIT / GETBITSETBIT设置指定位的值# 设置第7位为1SETBIT mykey71# → (integer) 0 ← 返回设置前的值0表示原来是0# 再次设置为1SETBIT mykey71# → (integer) 1 ← 返回设置前的值1表示原来已经是1# 设置为0清除位SETBIT mykey70# → (integer) 1 ← 返回设置前的值GETBIT获取指定位的值GETBIT mykey7# → (integer) 0 ← 当前值GETBIT mykey100← 不存在的位# → (integer) 0 ← 自动返回0基本操作示例# 用BitMap记录一周的签到情况0未签到1已签到SETBIT sign:user:123:20260501# 周一签到SETBIT sign:user:123:20260511# 周二签到SETBIT sign:user:123:20260520# 周三未签到SETBIT sign:user:123:20260531# 周四签到SETBIT sign:user:123:20260541# 周五签到SETBIT sign:user:123:20260550# 周六未签到SETBIT sign:user:123:20260561# 周日签到# 查看周三是否签到GETBIT sign:user:123:2026052# → (integer) 0 ← 未签到BITCOUNT统计为1的位数# 统计整个BitMap中1的个数BITCOUNT mykey# → (integer) 5 ← 5天签到# 统计指定字节范围内的1的个数BITCOUNT mykey00← 只统计第0个字节# → (integer) 5# 统计指定字节范围BYTE单位默认BITCOUNT mykey01← 第0和第1个字节BITCOUNT的start和end参数是字节偏移不是位偏移。这一点很容易搞混# 注意BITCOUNT的范围是字节不是位BITCOUNT mykey00# 第0个字节位0到位7BITCOUNT mykey01# 第0和第1个字节位0到位15# Redis 7.0 支持 BIT 单位BITCOUNT mykey07BIT# 位0到位7命令范围参数单位SETBIT/GETBIToffset位BITCOUNTstart end字节默认/ BIT7.0BITPOSstart end字节默认/ BIT7.0⚠️ 注意BITCOUNT统计的是字节范围不是位范围如果你只想统计某几位的1的个数需要用BITFIELD或者Lua脚本辅助。BITPOS查找第一个0或1的位置# 查找第一个1的位置BITPOS mykey1# → (integer) 0 ← 第0位是1# 查找第一个0的位置BITPOS mykey0# → (integer) 2 ← 第2位是0# 在指定字节范围内查找BITPOS mykey100← 在第0个字节中查找第一个1BITPOS的完整语法BITPOS key bit[start[end[BYTE|BIT]]]参数说明bit要查找的值0或1start起始字节偏移end结束字节偏移BYTE/BIT范围单位7.0实战案例查找用户第一次签到的日期# sign:user:123:202605 记录了用户5月每天的签到BITPOS sign:user:123:2026051# → (integer) 0 ← 第1天就签到了BITPOS sign:user:123:202605125# → 在第2-5字节范围查找第一个1BITOP位运算操作BITOP可以对多个BitMap执行位运算并将结果存储到目标keyBITOP operation destkey key[key...]支持四种位运算操作说明类比AND按位与交集OR按位或并集XOR按位异或对称差NOT按位取反只接受一个key补集# 三个BitMapSETBIT map101# 10000001SETBIT map171SETBIT map201# 10000010SETBIT map261SETBIT map301# 10000100SETBIT map351# AND三个map都为1的位BITOP AND result map1 map2 map3# → 只有第0位同时为1# OR任一map为1的位BITOP OR result map1 map2 map3# → 第0,5,6,7位都为1# XOR奇数个map为1的位BITOP XOR result map1 map2 map3# → 第5,6,7位只有1个map为1的位# NOT取反BITOP NOT result map1# → 第0位变0其他位变1实战案例计算连续3天都活跃的用户# 每天的活跃用户BitMap位偏移 用户IDSETBIT active:2026-05-2311# 用户1活跃SETBIT active:2026-05-2321# 用户2活跃SETBIT active:2026-05-2411# 用户1活跃SETBIT active:2026-05-2511# 用户1活跃SETBIT active:2026-05-2531# 用户3活跃# AND运算连续3天都活跃的用户BITOP AND active:3days active:2026-05-23 active:2026-05-24 active:2026-05-25# 查看结果BITCOUNT active:3days# → (integer) 1 ← 只有1个用户连续3天活跃用户1⚠️ 注意BITOP是O(N)时间复杂度N是最长BitMap的字节数。如果参与运算的BitMap很大比如1亿用户12.5MBBITOP的执行时间可能达到几十毫秒。不建议对超大BitMap频繁做BITOP。实际应用场景场景1用户签到每天用1bit记录某个用户是否签到365天只需46字节# 用户123在2026年5月26日签到第146天SETBIT sign:user:123:20261461# 查看用户123在5月26日是否签到GETBIT sign:user:123:2026146# → (integer) 1# 统计用户123全年签到天数BITCOUNT sign:user:123:2026# → (integer) 120 ← 签到了120天用户签到 BitMap 存储 一年365天 365 bit 46 字节 sign:user:123:2026: ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬─ ... ─┬──┐ │0 │1 │0 │0 │1 │1 │0 │1 │0 │1 │ ... │1 │ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴─ ... ─┴──┘ 1 2 3 4 5 6 7 8 9 10 ... 365 位偏移 日期 - 1第1天 → 位0 值 1表示签到0表示未签到 存储: 365位 ÷ 8 45.625 ≈ 46字节对比传统方案方案存储一年的数据1亿用户总存储MySQL表user_id date status~365行 × ~50字节~1.8TBRedis Set每天一个set365 × ~32字节~1.2TBRedis BitMap46字节~4.6GBBitMap的空间优势是碾压级的——1.8TB vs 4.6GB差了近400倍场景21亿用户活跃状态标记每个用户用1bit表示是否活跃1亿用户只需要12.5MB# 用户ID为1000的用户活跃SETBIT active:2026-05-2610000001# 统计今天的活跃用户数BITCOUNT active:2026-05-26# → (integer) 58000000 ← 5800万活跃用户# 查看用户1000是否活跃GETBIT active:2026-05-261000000# → (integer) 1 ← 活跃1亿用户活跃状态存储 用户ID范围: 1 ~ 100,000,000 BitMap大小: 100,000,000 bit 12.5 MB 对比: ┌──────────────────────┬──────────────┐ │ 方案 │ 存储大小 │ ├──────────────────────┼──────────────┤ │ MySQL表 │ ~3GB │ │ Redis Set存用户ID │ ~400MB │ │ Redis BitMap │ 12.5MB │ └──────────────────────┴──────────────┘ BitMap: 一天12.5MB一年365天 4.6GB Set: 一天400MB一年365天 146GB布隆过滤器基础BitMap的另一个重要应用是布隆过滤器Bloom Filter。布隆过滤器是一种概率型数据结构可以判断某个元素一定不存在或可能存在。布隆过滤器原理 1. 初始化: 一个全0的BitMap 2. 添加元素 hello: hash1(hello) 3 → SETBIT bitmap 3 1 hash2(hello) 7 → SETBIT bitmap 7 1 hash3(hello) 11 → SETBIT bitmap 11 1 3. 添加元素 world: hash1(world) 5 → SETBIT bitmap 5 1 hash2(world) 7 → SETBIT bitmap 7 1 (和hello冲突了) hash3(world) 15 → SETBIT bitmap 15 1 4. 查询 hello: 检查位3,7,11 → 全部为1 → 可能存在 ✓ 5. 查询 foo: 检查位2,8,14 → 位2为0 → 一定不存在 ✓ 6. 查询 bar: 检查位5,7,11 → 全部为1 → 可能存在 但实际上 bar 没有被添加过→ 误判布隆过滤器的特性特性说明判断不存在100%准确判断存在可能误判假阳性删除不支持删除一个元素可能影响其他元素空间效率极高远小于SetRedis 4.0 通过模块RedisBloom提供了原生的布隆过滤器支持# RedisBloom模块命令需要安装模块BF.ADD myfilter hello BF.EXISTS myfilter hello# → 1可能存在BF.EXISTS myfilter world# → 0一定不存在⚠️ 注意布隆过滤器的误判率取决于BitMap大小和哈希函数数量。BitMap越小、元素越多误判率越高。通常1%误判率下每个元素需要约10位空间。100万个元素大约需要1.2MB。Redis 3.2 BITFIELD命令BITFIELD命令允许对BitMap中的任意位宽的整数进行读写操作比SETBIT/GETBIT灵活得多BITFIELD key[GETtypeoffset][SETtypeoffset value][INCRBYtypeoffset increment][OVERFLOW WRAP|SAT|FAIL]GET读取任意位宽的整数# 读取从位0开始的8位无符号整数BITFIELD mykey GET u80# → 读取8位无符号整数一个字节# 读取从位16开始的32位有符号整数BITFIELD mykey GET i3216# → 读取32位有符号整数# 一次读取多个字段BITFIELD mykey GET u80GET u88GET u1616type参数格式u/i 位数类型说明范围u88位无符号0 ~ 255i88位有符号-128 ~ 127u1616位无符号0 ~ 65535i3232位有符号-2147483648 ~ 2147483647u6464位无符号0 ~ 2^64-1SET设置任意位宽的整数# 设置从位0开始的8位无符号整数为42BITFIELD mykey SET u8042# 设置从位8开始的16位无符号整数为1000BITFIELD mykey SET u1681000INCRBY自增操作# 将位0的8位无符号整数加1BITFIELD mykey INCRBY u801# 将位8的16位无符号整数加100BITFIELD mykey INCRBY u168100OVERFLOW溢出控制# WRAP回绕默认——像C语言一样溢出BITFIELD mykey OVERFLOW WRAP INCRBY u80200# SAT饱和——达到最大/最小值后不再变化BITFIELD mykey OVERFLOW SAT INCRBY u80200# FAIL失败——溢出时返回nilBITFIELD mykey OVERFLOW FAIL INCRBY u80200实战用BITFIELD实现计数器# 在一个BitMap中存储多个计数器# 位0-15: 计数器116位最大65535# 位16-31: 计数器216位# 位32-63: 计数器332位# 设置计数器1为100BITFIELD counters SET u160100# 计数器1加1BITFIELD counters INCRBY u1601# 读取所有计数器BITFIELD counters GET u160GET u1616GET i3232⚠️ 注意BITFIELD的offset参数是位偏移和SETBIT/GETBIT一致不是字节偏移。但BITCOUNT的范围参数是字节偏移。这又是Redis API一致性的一个坑。BitMap vs Set vs HyperLogLog选型对比对比维度BitMapSetHyperLogLog存储内容每个用户1位每个用户一个元素概率估算精确度100%精确100%精确约0.81%标准误差判断某用户是否存在O(1)O(1)不支持统计总数O(N)扫描O(1) SCARDO(1) PFCOUNT1亿用户存储12.5MB~400MB12KB交并差运算BITOP O(N)SINTER/SUNIONPFMERGE并集删除元素SETBIT为0SREM不支持删除用户ID要求必须是整数任意字符串任意字符串适用场景签到、活跃标记、布隆过滤器精确去重、成员判断UV估算选型决策树 你需要统计用户数量 │ ├── 需要精确统计 → 数据量多大 │ │ │ ├── 1亿 → Set │ └── 1亿 → BitMap │ └── 可以接受误差 → HyperLogLog12KB搞定一切 你需要判断某个用户是否在集合中 │ ├── 是 → BitMap整数ID或 Set字符串ID └── 不需要 → HyperLogLog 你需要做交并差运算 │ ├── 频繁运算 → Set原生支持O(N)但常数小 └── 偶尔运算 → BitMapBITOP实战用BitMap实现连续签到天数统计这是一个经典的生产级案例。需求统计用户连续签到的天数从今天往前数。数据模型# 用户每天的签到用1bit存储# key格式: sign:{userId}:{year}# 位偏移 第N天0-based签到操作# 获取今天是今年的第几天# 2026-05-26 是第146天0-based: 145SETBIT sign:123:20261451统计连续签到天数这里需要用Lua脚本因为需要逐位检查从今天往前有多少个1EVAL local key KEYS[1] local today tonumber(ARGV[1]) -- 今天的位偏移 local consecutive 0 for i today, 0, -1 do if redis.call(GETBIT, key, i) 1 then consecutive consecutive 1 else break -- 遇到0就停止 end end return consecutive 1sign:123:2026145优化方案批量BITFIELD逐位GETBIT效率不高可以用BITFIELD一次读取多个位EVAL local key KEYS[1] local today tonumber(ARGV[1]) local consecutive 0 -- 每次读取32位减少Redis调用次数 local bitsPerRead 32 for start today, 0, -bitsPerRead do local readBits math.min(bitsPerRead, start 1) local result redis.call(BITFIELD, key, GET, u .. bitsPerRead, start - bitsPerRead 1) if result and result[1] then local value result[1] -- 从高位到低位逐位检查 for bit readBits - 1, 0, -1 do if bit.band(value, bit.lshift(1, bit)) ~ 0 then consecutive consecutive 1 else return consecutive end end end end return consecutive 1sign:123:2026145连续签到统计流程 sign:123:2026: 位: ... 142 143 144 145 146 147 ... 值: ... 1 1 1 1 0 0 ... 从今天(位145)往前检查: 位145: 1 → consecutive 1 位144: 1 → consecutive 2 位143: 1 → consecutive 3 位142: 1 → consecutive 4 位141: 0 → 停止 结果: 连续签到4天签到功能完整代码Pythonimportredisfromdatetimeimportdatetime rredis.Redis()defsign_in(user_id):用户签到yeardatetime.now().year day_of_yeardatetime.now().timetuple().tm_yday-1# 0-basedkeyfsign:{user_id}:{year}r.setbit(key,day_of_year,1)returnTruedefget_sign_count(user_id,yearNone):统计全年签到天数yearyearordatetime.now().year keyfsign:{user_id}:{year}returnr.bitcount(key)defget_consecutive_days(user_id):统计连续签到天数yeardatetime.now().year day_of_yeardatetime.now().timetuple().tm_yday-1keyfsign:{user_id}:{year}script local key KEYS[1] local today tonumber(ARGV[1]) local consecutive 0 for i today, 0, -1 do if redis.call(GETBIT, key, i) 1 then consecutive consecutive 1 else break end end return consecutive returnr.eval(script,1,key,day_of_year)# 使用sign_in(123)# 签到print(get_sign_count(123))# 全年签到天数print(get_consecutive_days(123))# 连续签到天数本章小结BitMap 核心要点 ┌──────────────────────────────────────────────────┐ │ 本质: String 类型的二进制视图 │ │ │ │ 核心命令: │ │ • SETBIT key offset value → 设置某位 │ │ • GETBIT key offset → 获取某位 │ │ • BITCOUNT key [start end] → 统计1的个数 │ │ • BITPOS key bit [start] → 查找第一个0/1 │ │ • BITOP op dest key [key] → 位运算 │ │ • BITFIELD key ... → 任意位宽整数操作 │ │ │ │ 空间效率: │ │ • 1亿用户活跃状态 → 12.5MB │ │ • 1年签到记录 → 46字节/人 │ │ │ │ 适用场景: │ │ • 用户签到、活跃标记 │ │ • 布隆过滤器 │ │ • 特征标记用户标签、权限位 │ │ • 连续状态统计 │ │ │ │ 注意: │ │ ✗ 不适合稀疏数据offset过大会浪费内存 │ │ ✗ 用户ID必须是整数 │ │ ✗ BITCOUNT范围是字节偏移不是位偏移 │ └──────────────────────────────────────────────────┘BitMap是Redis中性价比最高的数据结构之一——用极小的空间实现海量的布尔标记。但它也有局限只适合整数ID、不适合稀疏数据、不支持精确删除。在实际使用中要根据场景选择BitMap、Set还是HyperLogLog。上一篇【第59篇】SORT命令——Redis里的万能排序指令下一篇【第61篇】 慢查询日志——找出Redis性能瓶颈的利器明日更新敬请期待