Redis 命令及数据类型 -- Bitmap

摘要

Bitmap 数据类型

  • Redis Bitmap 并非独立数据类型,而是基于 String 类型的位操作扩展

  • String 底层是字节数组,Bitmap 就是对数组中单个 bit 做读写(bit 只有 0/1 两个值)

Bitmap 命令使用方式

  • 核心围绕「位设置、位查询、位统计、位运算」四类命令,是日常使用的基础

1. 位设置:SETBIT key offset value

  • 给指定 key 的第 offset 位设 0/1(offset 从 0 开始,支持超大偏移量,Redis 会自动扩容)

  • offset 从左往右递增,从左到右为 0、1、2…,至少申请 8bit 空间,不足 8bit 时,会自动扩展到 8bit 即 1byte

1
2
3
4
5
6
SETBIT bitkey 1 1 # 实际的bit为 01000000
SETBIT bitkey 10 1 # 实际的bit为 0100000000100000
# 操作 String
SET k1 v1 # 实际的bit为 0111011000110001
SETBIT k1 1 0 # 实际bit为 0011011000110001
GET k1 # 输出 61

2. 位查询:GETBIT key offset

  • 查询指定偏移量的位值,不存在的 offset 默认返回 0

1
2
3
GETBIT bitkey 1
# 输出
(integer) 1

3. 位统计:BITCOUNT key [start end]

  • 统计 key 中值为 1 的 bit 总数,可选按字节范围(start/end 是字节索引)筛选

1
2
3
BITCOUNT bitkey
# 等价于
BITCOUNT bitkey 0 -1

4. 位运算:BITOP op destkey key1 key2...

  • 对多个 Bitmap 做 与(AND)、或(OR)、异或(XOR)、非(NOT)运算,结果存入 destkey

1
2
# 将key1 与 key2 做按位与运算,结果存入 destkey
BITOP AND destkey key1 key2

5. 位查找:BITPOS key value [start end]

  • 查找第一个值为 0/1 的 bit 偏移量,快速定位目标位

1
2
3
BITPOS bitkey 1
# 等价于
BITPOS bitkey 1 0 -1

6. BITFIELD 批量位操作

• BITFIELD 是 Redis 用于把一个 字符串值视为一个由二进制“位数组”组成的存储区,并对其中任意指定位置的整数域进行读取、写入、自增等操作的命令。
• 这些整数域可以是任意位宽(例如 1 位、4 位、8 位、31 位、63 位等),可指定为有符号(signed)或无符号(unsigned)。
• BITFIELD 命令支持在一次调用中执行多个操作,并将结果按操作顺序返回。

1
2
3
4
5
BITFIELD key [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
<SET encoding offset value | INCRBY encoding offset increment>
[GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
<SET encoding offset value | INCRBY encoding offset increment>
...]]

• 参数说明(核心部分)

参数 含义
key 操作的 Redis 字符串键
GET encoding offset 从指定位偏移量读取一个整数
SET encoding offset value 在指定位置写入整数
INCRBY encoding offset increment 在指定位置对整数做增量操作
OVERFLOW WRAP/SAT/FAIL 配置随后的算数操作溢出行为

• 数据类型(encoding):用于指定整数的位宽和符号类型

前缀 含义 示例
u<number> 无符号整数(unsigned),占位 bits = number u5 — 5 位无符号整数
i<number> 有符号整数(signed),占位 bits = number i10 — 10 位有符号整数

• 溢出(OVERFLOW):默认算数操作中可能发生溢出,OVERFLOW 允许你控制处理策略。注意,这部分必须在后续的 SET/INCRBY 操作之前指定。

策略 行为
WRAP 环绕(默认)溢出按环形计数处理
SAT 饱和,在边界值保持最大/最小
FAIL 溢出时操作失败并返回错误
1
2
# 4 位无符号整数加 1 时,如果超过 15,则保持 15(饱和)。
BITFIELD key OVERFLOW SAT INCRBY u4 0 1

• 返回值: BITFIELD 会为每个子命令返回一个整数数组,数组各元素按操作顺序对应执行结果

1
2
3
4
BITFIELD mykey INCRBY i5 100 1 GET u4 0
# 输出
1) (integer) 1
2) (integer) 0

综合示例

  • 示例 1. 设置并读取简单整数

1
2
3
4
5
6
7
8
9
10
11
> SET mykey "" # 此时mykey 的值是空字符串,长度为 0
# SET u4 0 7:在 key mykey 偏移 0 位置设置 4 位无符号整数值为 7,即前4位变成 0111,因为至少8bit,所以实际值是 01110000
# GET u4 0:读取 key mykey 偏移 0 位的 4 位无符号整数,此时 mykey 虽然是 01110000,但这里指定只读前4位,即 0111,所以转换为二进制就是 7
> BITFIELD mykey SET u4 0 7 GET u4 0
# 输出
1) (integer) 0 # set命令的返回值,这里返回set前的值
2) (integer) 7 # get命令的返回值
# 设置有符号整数
> BITFIELD mykey SET i4 0 -2 GET i4 0
1) (integer) 7 # 返回set前的值,之前是7,即 0111,set后变为 -2,即 1110
2) (integer) -2 # get命令的返回值

负数的二进制表示

  • 以 -2 为例,先写出 +2 的二进制,以8位为例,就是 00000010,4位就是 0010
  • 按位取反(得到反码),例如 00000010,按位取反就是 11111101
  • 加 1(得到补码),11111101 + 1,得到 11111110,这就是 -2 的二进制表示
  • 如果是4位,则 -2 就是 1110
  • 示例 2. 自增计数器

1
2
3
4
5
6
7
# 如果 counter 之前为空,则视为 0
# INCRBY u8 8 5:在 key counter 的第 8 位(下一个字节开头处)上按 8 位 unsigned 类型自增 5
# GET u8 8:读取 key counter 的第 8 位(下一个字节开头处)的 8 位无符号整数,返回这个整数的新值
BITFIELD counter INCRBY u8 8 5 GET u8 8
# 输出
1) (integer) 5 # INCRBY 的返回值,这里返回的是增加的值,而不是计算后的值
2) (integer) 5
  • 示例 3. 带溢出控制的操作

1
2
3
4
5
6
7
# OVERFLOW SAT: 设置溢出策略为 SAT(饱和)
# INCRBY u4 0 20: 对 4 位无符号整数(最大 15)加 20,此时肯定会溢出,由于过 15 上限,结果返回 15(饱和)
# GET u4 0:读取 4 位无符号整数,即15
BITFIELD limits OVERFLOW SAT INCRBY u4 0 20 GET u4 0
# 输出
1) (integer) 15
2) (integer) 15
  • 示例 4. 批量多个操作

1
2
3
4
5
6
7
8
9
# GET u4 0 GET u4 4: 依次读取多个不同 bit 偏移量上的小整数
# SET u4 8 3 INCRBY i5 16 1: 写入、增量操作可以混合处理
# 返回结果数组对应每个子命令顺序返回结果
BITFIELD events GET u4 0 GET u4 4 SET u4 8 3 INCRBY i5 16 1
# 输出
1) (integer) 0 # events原先为空,所以这里返回0
2) (integer) 0 # events原先为空,所以这里返回0
3) (integer) 0 # events原先为空,这里返回原先的值,所以还是0,但此时实际的值是 0000000000110000
4) (integer) 1 # 返回增加的值,即 1,但此时实际的值是 000000000011000000001000,即占用了3个字节

核心使用场景 + 实操举例(贴合开发实战)

  • Bitmap 的核心优势是极致省内存+高效统计(1 个字节=8 个 bit,存储 1000 万条状态仅需约 1.2MB),以下是高频场景

场景1: 用户签到/打卡(最经典场景)

• 需求:记录用户每日签到状态,查询某用户某天是否签到、统计某用户月度签到次数
• 设计:key 格式 user:sign:uid:202512(用户2025年12月签到),offset 为日期(1号=0、2号=1…31号=30),签到设 1、未签到默认 0
• 实操命令:

  1. 12月1号签到:SETBIT user:sign:uid:202512 0 1

  2. 查询12月1号是否签到:GETBIT user:sign:uid:202512 0(返回1=签到)

  3. 统计12月总签到次数:BITCOUNT user:sign:uid:202512

• 优势:1个用户1个月签到仅占 4 字节(31 bit),百万用户月度签到仅占约 3.9MB,远超数据库存储的性价比。

场景2: 日活/周活/月活(DAU/WAU/MAU)统计(高并发场景首选)

• 需求:统计每日访问平台的用户数,快速计算周活(7天内至少访问1次)、月活,去重统计
• 设计:按日期建 Bitmap,key 格式 active:user:20251217(当日活跃),offset 设为用户唯一ID(需确保ID是连续或可映射为数字,避免超大偏移量),用户访问则设 1
• 实操命令:

  1. 用户ID 10086 12月17日访问:SETBIT active:user:20251217 10086 1

  2. 统计12月17日日活:BITCOUNT active:user:20251217

  3. 统计12月15-17日周活(3天内活跃):BITOP OR active:user:20251215_17 active:user:20251215 active:user:20251216 active:user:20251217 → 再执行 BITCOUNT active:user:20251215_17

• 优势:百万级用户日活统计,单 Bitmap 仅占约 125KB,位运算合并统计速度毫秒级,远快于数据库 group by 去重。

场景3: 功能开关/状态标记(多维度轻量标记)

• 需求:给用户标记多类轻量状态(如是否开通会员、是否绑定手机、是否参与活动),无需单独存多个key
• 设计:1个key对应1个用户,key 格式 user:status:10086,不同 offset 对应不同状态(offset0=是否绑定手机、offset1=是否会员、offset2=是否参与活动),1=是、0=否
• 实操命令:

  1. 给用户10086绑定手机:SETBIT user:status:10086 0 1

  2. 开通会员:SETBIT user:status:10086 1 1

  3. 查询是否是会员:GETBIT user:status:10086 1

• 优势:1个key承载用户N个状态,无需维护多个 String/Hash,查询和修改均为O(1),极简高效。

场景4: 布隆过滤器底层实现(核心依赖Bitmap)

• 需求:实现海量数据的快速去重判断(如缓存穿透防护、海量URL去重),允许极小误判率,不允许漏判
• 设计:用1个大 Bitmap 作为底层存储,配合多个哈希函数 —— 数据存入时,通过多个哈希函数算出多个 offset,将对应 bit 设为1;查询时,若所有哈希对应的 offset 都是1,则大概率存在,否则一定不存在
• 实操:Redis 7 可直接用 Bitmap 手动实现,也可结合 RedisBloom 扩展(更易用),核心原理是 Bitmap 的位设置与查询。
• 优势:存储1亿条数据,误判率5%的布隆过滤器,仅需约 12MB 内存,查询速度极致快。

注意事项(避坑关键)

  1. offset 不要无限制过大:虽 Redis 支持超大 offset,但过大(如超过10亿)会导致 Bitmap 占用内存骤增,需合理规划 offset 范围(如用户ID做哈希映射压缩)

  2. 避免单 key 过大:单个 Bitmap 建议控制在1GB内(对应约85亿 bit),过大易导致Redis持久化/迁移耗时过长

  3. 注意编码兼容:Bitmap 基于 String,Redis 会自动用 RAW 编码存储,无需手动设置

Bitmap 命令

  • SpringBoot 的 StringRedisTemplate.opsForValue() 中 Bitmap 数据类型 的操作方法与 Redis 原生命令的对应关系如下:

注意这里一定要用 StringRedisTemplate 来操作 Bitmap

写操作(位修改)

方法功能 方法 Redis 原始命令 备注
设置指定偏移量的位 setBit(K key, long offset, boolean value) SETBIT key offset value 返回旧值(0 / 1),offset 从 0 开始

offset 表示 第几位(bit),不是字节

读操作(位查询)

方法功能 方法 Redis 原始命令 备注
获取指定偏移量的位 getBit(K key, long offset) GETBIT key offset 返回 0 / 1,不会修改数据

批量位操作(位字段 BitField)

  • 位字段读/写/自增

方法功能 方法 Redis 原始命令 备注
位字段操作(读/写/自增) bitField(K key, BitFieldSubCommands subCommands) BITFIELD key ... 原子执行多个子命令
  • 示例

1
2
3
4
5
BITFIELD limits
OVERFLOW SAT
SET u4 0 3
INCRBY u4 0 20
GET u4 0
1
2
3
4
5
6
7
8
9
// SET u4 0 3
BitFieldSubCommands.BitFieldSet bitFieldSet = BitFieldSubCommands.BitFieldSet.create(BitFieldSubCommands.BitFieldType.unsigned(4), BitFieldSubCommands.Offset.offset(0), 3);
// OVERFLOW SAT INCRBY u4 0 20
BitFieldSubCommands.BitFieldIncrBy bitFieldIncrBy = BitFieldSubCommands.BitFieldIncrBy.create(BitFieldSubCommands.BitFieldType.unsigned(4), BitFieldSubCommands.Offset.offset(0), 20, BitFieldSubCommands.BitFieldIncrBy.Overflow.SAT);
// GET u4 0
BitFieldSubCommands.BitFieldGet bitFieldGet = BitFieldSubCommands.BitFieldGet.create(BitFieldSubCommands.BitFieldType.unsigned(4), BitFieldSubCommands.Offset.offset(0));

// 获取结果,每个子操作返回一个结果
List<Long> limits = redisTemplate.opsForValue().bitField("limits", BitFieldSubCommands.create(bitFieldSet, bitFieldIncrBy, bitFieldGet));

Bitmap 常用但 Spring 未直接封装的命令

  • Spring Data Redis 中通常通过 RedisCallback 或 execute 调用这些命令。

Redis 命令 功能 说明
BITCOUNT key [start end] 统计 bit=1 的数量 常用于活跃用户统计
BITPOS key bit [start end] 查找第一个 0/1 的位置 常用于分配位
BITOP AND/OR/XOR/NOT 位运算 多 bitmap 计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.example.demo.bitmap;

import com.example.demo.CommonUtil;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

@Component
public class BitmapUtil {

@Autowired
protected StringRedisTemplate redisTemplate;
/**
* BITCOUNT key [start end]
* <p>
* 统计 bit=1 的数量
*/
public Long bitCount(String key) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.stringCommands().bitCount(key.getBytes(StandardCharsets.UTF_8))
);
}

public Long bitCount(String key, long start, long end) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.stringCommands().bitCount(key.getBytes(), start, end)
);
}

/**
* BITPOS key bit [start] [end]
* <p>
* bit = false → 查找第一个 0
* bit = true → 查找第一个 1
* <p>
* 返回值是 bit 索引(不是 byte)
*/
public Long bitPos(String key, boolean bit) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.stringCommands().bitPos(key.getBytes(), bit)
);
}

public Long bitPos(String key, boolean bit, long start, long end) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.stringCommands().bitPos(key.getBytes(), bit, Range.open(start, end))
);
}

/**
* BITOP operation destKey key [key ...]
* <p>
* operation: AND\OR\XOR\NOT
* <p>
* 运算结果保存在 destKey 中
*/
public Long bitOp(String destKey, RedisStringCommands.BitOperation operation, String... sourceKeys) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
byte[][] keys = Arrays.stream(sourceKeys)
.map(String::getBytes)
.toArray(byte[][]::new);

return connection.stringCommands().bitOp(
operation,
destKey.getBytes(),
keys
);
});
}
}