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
7
8
9
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

# bitmap 实际上是 string
TYPE bitkey # 输出 string

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

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

  • 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,远超数据库存储的性价比。

  • 劣势:若想查询该用户本月内都哪天签到了,即要查看bitmap哪些位为1,则bitmap不支持这个命令,可以在业务端实现。如需要精确查询和聚合统计则需要同步数据到关系型数据库。

场景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

  4. 统计12月15-17日每天都登录的用户数:BITOP AND active:user:20251215_17 active:user:20251215 active:user:20251216 active:user:20251217 → 再执行 BITCOUNT active:user:20251215_17

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

  • 劣势:若想查看本月内哪些用户登录过,则需要遍历 Bitmap 的所有 offset 位,效率较低。如需要精确查询和聚合统计则需要同步数据到关系型数据库。

场景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),极简高效。

  • 劣势:若想查看用户所有状态,则需要遍历所有 offset 的位,效率较低,另外统计哪些用户开启了某个状态也比较麻烦。如需要精确查询和聚合统计则需要同步数据到关系型数据库。

场景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,不会修改数据

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
);
});
}
}