Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis核心数据结构
  • Redis持久化机制
  • Redis高可用架构
  • Redis分布式锁
  • Redis缓存设计
  • Redis大Key与热Key
  • Redis限流
  • Redis IO多路复用
  • Redis过期删除策略
  • Redis Bitmap
    • 一、核心概念
      • 1.1 本质
      • 1.2 核心优势
      • 1.3 适用场景特征
    • 二、核心命令详解
      • 2.1 SETBIT:设置指定bit位的值
      • 2.1.1 语法
      • 2.1.2 参数说明
      • 2.1.3 功能
      • 2.1.4 返回值
      • 2.1.5 示例
      • 2.2 GETBIT:获取指定bit位的值
      • 2.2.1 语法
      • 2.2.2 参数说明
      • 2.2.3 功能
      • 2.2.4 返回值
      • 2.2.5 示例
      • 2.3 BITCOUNT:统计指定范围内的1的个数
      • 2.3.1 语法
      • 2.3.2 参数说明
      • 2.3.3 功能
      • 2.3.4 返回值
      • 2.3.5 示例
      • 2.4 BITOP:对多个Bitmap执行位运算
      • 2.4.1 语法
      • 2.4.2 参数说明
      • 2.4.3 功能
      • 2.4.4 返回值
      • 2.4.5 示例
      • 2.5 BITPOS:查找指定值的第一个bit位偏移量
      • 2.5.1 语法
      • 2.5.2 参数说明
      • 2.5.3 功能
      • 2.5.4 返回值
      • 2.5.5 示例
    • 三、典型使用场景
      • 3.1 用户签到统计
      • 3.1.1 场景需求
      • 3.1.2 实现方案
      • 3.1.3 核心操作
      • 3.2 用户在线状态判断
      • 3.2.1 场景需求
      • 3.2.2 实现方案
      • 3.2.3 核心操作
      • 3.3 数据权限控制
      • 3.3.1 场景需求
      • 3.3.2 实现方案
      • 3.3.3 核心操作
      • 3.4 布隆过滤器(Bloom Filter)底层实现
      • 3.4.1 场景需求
      • 3.4.2 实现方案
      • 3.4.3 基于Redis Bitmap的实现示例
    • 四、实现原理
      • 4.1 底层存储:基于String的二进制安全特性
      • 4.2 高效性保障:位运算的底层优化
      • 4.3 内存占用计算
    • 五、注意事项
      • 5.1 偏移量的合理设计
      • 5.2 键的生命周期管理
      • 5.3 并发操作的安全性
      • 5.4 误判率问题(布隆过滤器场景)
      • 5.5 不适合的场景
    • 六、总结
  • 《Redis》笔记
Tavio
2024-10-10
目录

Redis Bitmap

Redis位图(Bitmap)是一种基于字符串类型的高效数据结构,它通过将字符串中的每个字节拆分为8个二进制位(bit),利用这些bit位来表示和存储布尔类型的信息(0或1)。相比传统的字符串、哈希等结构,Bitmap在存储海量布尔型数据时具有极高的空间效率,同时支持高效的位运算操作,广泛应用于用户签到、在线状态判断、数据统计等场景。

# 一、核心概念

# 1.1 本质

Bitmap的本质是Redis的字符串(String)类型,因为Redis的字符串是二进制安全的,其底层存储的是字节序列。Bitmap通过对这些字节序列的bit位进行操作,实现了“以bit为单位”的数据存储和管理。例如,一个长度为1的字符串(1字节)可以表示8个bit位,对应8个布尔值;长度为2的字符串可以表示16个bit位,以此类推。

# 1.2 核心优势

  • 空间效率极高:1个字节可存储8个布尔值,相比使用哈希表(Hash)存储(每个键值对至少占用几十字节),空间占用降低一个数量级。例如,存储1000万用户的签到状态,仅需约1.2MB(10000000 / 8 / 1024 ≈ 1220KB)。
  • 操作高效:支持单个bit位的快速设置、查询,以及多个Bitmap之间的位运算(与、或、异或等),所有操作的时间复杂度均为O(1)或O(n)(n为bit位长度,效率远高于其他结构)。
  • 兼容性好:基于String类型实现,无需额外的底层存储支持,所有支持Redis String的客户端均可直接操作Bitmap。

# 1.3 适用场景特征

适合存储“是否”型数据(如是否签到、是否在线、是否读过某篇文章),且数据标识为连续或可枚举的整数(如用户ID、日期偏移量、商品ID)。若数据标识为非整数(如用户名、UUID),需先进行映射转换为整数,否则无法高效使用Bitmap。

# 二、核心命令详解

Redis提供了一系列专门用于操作Bitmap的命令,涵盖bit位的设置、查询、统计、位运算等核心功能。以下是常用命令的详细说明:

# 2.1 SETBIT:设置指定bit位的值

# 2.1.1 语法

SETBIT key offset value

# 2.1.2 参数说明

  • key:Bitmap对应的键名。
  • offset:bit位的偏移量,从0开始(0表示第一个bit位),支持非负整数(理论上无上限,仅受Redis内存限制)。
  • value:待设置的bit值,只能是0或1。

# 2.1.3 功能

将key对应的Bitmap中,偏移量为offset的bit位设置为value。若key不存在,会先创建一个空字符串(所有bit位默认值为0);若offset超过当前字符串的长度,Redis会自动在字符串末尾补0,直到长度足以覆盖offset对应的bit位。

# 2.1.4 返回值

该bit位在设置前的旧值(0或1)。

# 2.1.5 示例

# 设置用户ID为100的用户在2026-01-13的签到状态为1(已签到)
# 键名:sign:20260113(表示2026年01月13日的签到表)
# 偏移量:100(用户ID),值:1
127.0.0.1:6379> SETBIT sign:20260113 100 1
(integer) 0  # 该bit位之前为0(未签到)

# 再次设置同一偏移量,返回旧值1
127.0.0.1:6379> SETBIT sign:20260113 100 1
(integer) 1
1
2
3
4
5
6
7
8
9

# 2.2 GETBIT:获取指定bit位的值

# 2.2.1 语法

GETBIT key offset

# 2.2.2 参数说明

  • key:Bitmap对应的键名。
  • offset:bit位的偏移量(从0开始)。

# 2.2.3 功能

获取key对应的Bitmap中,偏移量为offset的bit位的值。若key不存在,视为所有bit位为0的Bitmap;若offset超过当前字符串的长度,返回0。

# 2.2.4 返回值

指定bit位的值(0或1)。

# 2.2.5 示例

# 获取用户ID100在2026-01-13的签到状态
127.0.0.1:6379> GETBIT sign:20260113 100
(integer) 1  # 已签到

# 获取不存在的用户ID200的签到状态(返回0)
127.0.0.1:6379> GETBIT sign:20260113 200
(integer) 0
1
2
3
4
5
6
7

# 2.3 BITCOUNT:统计指定范围内的1的个数

# 2.3.1 语法

BITCOUNT key [start end]

# 2.3.2 参数说明

  • key:Bitmap对应的键名。
  • start/end:可选参数,指定统计的字节范围(注意:是字节范围,不是bit位范围),默认统计整个字符串的所有字节。start和end支持负数,-1表示最后一个字节,-2表示倒数第二个字节,以此类推。

# 2.3.3 功能

统计key对应的Bitmap中,指定字节范围内所有bit位中值为1的个数。若需要统计指定bit位范围,需先将bit位范围转换为字节范围(例如,统计bit 0-15,对应字节0-1)。

# 2.3.4 返回值

指定范围内1的个数。

# 2.3.5 示例

# 统计2026-01-13的总签到人数(所有bit位中1的个数)
127.0.0.1:6379> BITCOUNT sign:20260113
(integer) 568  # 当天有568人签到

# 统计前10个字节(对应bit 0-79)中1的个数(即用户ID 0-79中的签到人数)
127.0.0.1:6379> BITCOUNT sign:20260113 0 9
(integer) 32
1
2
3
4
5
6
7

# 2.4 BITOP:对多个Bitmap执行位运算

# 2.4.1 语法

BITOP operation destkey key [key ...]

# 2.4.2 参数说明

  • operation:位运算类型,支持AND(与)、OR(或)、XOR(异或)、NOT(非)。注意:NOT运算仅支持单个key,其他运算支持多个key。
  • destkey:运算结果存储的目标键名(会覆盖已有键)。
  • key [key ...]:参与运算的源Bitmap键名。

# 2.4.3 功能

对多个源Bitmap执行指定的位运算,将结果存储到destkey中。当多个源Bitmap的长度不一致时,Redis会以最长的Bitmap为准,较短的Bitmap在末尾补0后再进行运算。

# 2.4.4 返回值

目标键destkey对应的字符串长度(以字节为单位)。

# 2.4.5 示例

# 场景:统计2026-01-13和2026-01-14两天都签到的用户(交集)
# 1. 假设已有两天的签到Bitmap:sign:20260113、sign:20260114
# 2. 执行AND运算,结果存入sign:20260113_14_both
127.0.0.1:6379> BITOP AND sign:20260113_14_both sign:20260113 sign:20260114
(integer) 1250  # 结果Bitmap长度为1250字节(对应10000个bit位,即支持10000个用户ID)

# 3. 统计两天都签到的人数
127.0.0.1:6379> BITCOUNT sign:20260113_14_both
(integer) 320  # 320人连续两天签到

# 场景:统计2026-01-13或2026-01-14至少签到一天的用户(并集)
127.0.0.1:6379> BITOP OR sign:20260113_14_either sign:20260113 sign:20260114
(integer) 1250
127.0.0.1:6379> BITCOUNT sign:20260113_14_either
(integer) 780  # 780人至少签到一天
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.5 BITPOS:查找指定值的第一个bit位偏移量

# 2.5.1 语法

BITPOS key value [start end]

# 2.5.2 参数说明

  • key:Bitmap对应的键名。
  • value:要查找的bit值(0或1)。
  • start/end:可选参数,指定查找的字节范围(默认查找整个字符串),支持负数(-1表示最后一个字节)。

# 2.5.3 功能

在key对应的Bitmap中,查找指定字节范围内第一个值为value的bit位的偏移量。若未找到,返回-1。

# 2.5.4 返回值

第一个符合条件的bit位偏移量,未找到则返回-1。

# 2.5.5 示例

# 查找2026-01-13第一个签到的用户(第一个值为1的bit位)
127.0.0.1:6379> BITPOS sign:20260113 1
(integer) 5  # 用户ID为5的用户是当天第一个签到的

# 查找2026-01-13中,字节10-20(bit 80-160)范围内第一个未签到的用户(值为0的bit位)
127.0.0.1:6379> BITPOS sign:20260113 0 10 20
(integer) 83  # 用户ID为83的用户未签到,且是该范围内第一个未签到的
1
2
3
4
5
6
7

# 三、典型使用场景

Bitmap的空间效率和高效位运算特性,使其在多个场景中具有不可替代的优势。以下是最常见的应用场景:

# 3.1 用户签到统计

# 3.1.1 场景需求

记录用户每天的签到状态,支持查询单个用户某天是否签到、统计用户连续签到天数、统计某一天的总签到人数、统计某段时间内的签到次数等。

# 3.1.2 实现方案

  • 键名设计:以“sign:日期”为键名(如sign:20260113),日期格式为年月日(YYYYMMDD)。
  • 偏移量设计:以用户ID为偏移量(假设用户ID为整数)。
  • 值设计:1表示已签到,0表示未签到。

# 3.1.3 核心操作

# 1. 用户ID 100 2026-01-13 签到
SETBIT sign:20260113 100 1

# 2. 查询用户ID 100 2026-01-13 是否签到
GETBIT sign:20260113 100

# 3. 统计2026-01-13 总签到人数
BITCOUNT sign:20260113

# 4. 统计用户ID 100 2026年1月(1-31日)的签到次数
# 遍历1-31日的键,执行GETBIT并累加1的个数
# 或使用BITOP OR合并31天的Bitmap,再统计1的个数(但需注意合并后无法区分具体天数)

# 5. 统计用户ID 100 连续签到天数(从当前日期往前推)
current_date = 20260113
continuous_days = 0
while True:
    key = f"sign:{current_date}"
    if GETBIT key 100 == 1:
        continuous_days += 1
        current_date = 前一天日期
    else:
        break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.2 用户在线状态判断

# 3.2.1 场景需求

实时记录用户的在线/离线状态,支持快速查询单个用户状态、统计当前在线用户总数。

# 3.2.2 实现方案

  • 键名设计:使用固定键名(如online:status),无需按日期拆分(因为状态是实时更新的)。
  • 偏移量设计:以用户ID为偏移量。
  • 值设计:1表示在线,0表示离线。

# 3.2.3 核心操作

# 1. 用户ID 200 上线,设置状态为1
SETBIT online:status 200 1

# 2. 用户ID 200 下线,设置状态为0
SETBIT online:status 200 0

# 3. 查询用户ID 200 是否在线
GETBIT online:status 200

# 4. 统计当前在线用户总数
BITCOUNT online:status
1
2
3
4
5
6
7
8
9
10
11

优势:相比使用Hash存储(如online:user {200:1, 201:0}),Bitmap节省90%以上的空间,且统计在线人数的效率更高(BITCOUNT vs HCOUNT)。

# 3.3 数据权限控制

# 3.3.1 场景需求

控制用户对多个资源的访问权限(如菜单、接口、文件),支持快速判断用户是否拥有某资源的权限,以及统计拥有某资源权限的用户数。

# 3.3.2 实现方案

  • 键名设计:以“permission:资源ID”为键名(如permission:menu:101,表示菜单101的权限表)。
  • 偏移量设计:以用户ID为偏移量。
  • 值设计:1表示拥有权限,0表示无权限。

# 3.3.3 核心操作

# 1. 给用户ID 300 分配菜单101的权限
SETBIT permission:menu:101 300 1

# 2. 撤销用户ID 300 对菜单101的权限
SETBIT permission:menu:101 300 0

# 3. 判断用户ID 300 是否能访问菜单101
GETBIT permission:menu:101 300

# 4. 统计拥有菜单101权限的用户总数
BITCOUNT permission:menu:101

# 5. 统计同时拥有菜单101和菜单102权限的用户数(交集)
BITOP AND permission:menu:101_102 permission:menu:101 permission:menu:102
BITCOUNT permission:menu:101_102
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.4 布隆过滤器(Bloom Filter)底层实现

# 3.4.1 场景需求

快速判断一个元素是否存在于海量数据集合中(如判断用户是否为新用户、判断URL是否已爬取、判断订单号是否存在),允许一定的误判率,但要求极高的查询效率和空间效率。

# 3.4.2 实现方案

布隆过滤器的核心是多个哈希函数 + Bitmap:

  • 将元素通过多个哈希函数计算得到多个整数偏移量。
  • 将Bitmap中这些偏移量对应的bit位设置为1。
  • 查询时,若所有偏移量对应的bit位均为1,则元素“可能存在”;若有一个为0,则元素“一定不存在”。

# 3.4.3 基于Redis Bitmap的实现示例

import hashlib

def hash_functions(element, num_hashes, bitmap_length):
    """生成num_hashes个哈希偏移量"""
    offsets = []
    for i in range(num_hashes):
        # 结合i生成不同的哈希值
        hash_val = hashlib.md5(f"{element}:{i}".encode()).hexdigest()
        # 转换为整数偏移量,取模bitmap_length确保在范围内
        offset = int(hash_val, 16) % bitmap_length
        offsets.append(offset)
    return offsets

# 初始化布隆过滤器(Bitmap长度为1000000bit,3个哈希函数)
BITMAP_KEY = "bloom:filter"
BITMAP_LENGTH = 1000000
NUM_HASHES = 3

# 1. 向布隆过滤器中添加元素(如用户ID:user_123456)
element = "user_123456"
offsets = hash_functions(element, NUM_HASHES, BITMAP_LENGTH)
for offset in offsets:
    redis_client.setbit(BITMAP_KEY, offset, 1)

# 2. 查询元素是否存在
def is_exist(element):
    offsets = hash_functions(element, NUM_HASHES, BITMAP_LENGTH)
    for offset in offsets:
        if redis_client.getbit(BITMAP_KEY, offset) == 0:
            return False  # 一定不存在
    return True  # 可能存在

print(is_exist("user_123456"))  # True(可能存在)
print(is_exist("user_654321"))  # False(一定不存在)
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

优势:相比传统的集合(Set),布隆过滤器的空间效率极高(存储100万元素,误判率1%,仅需约1.2MB空间),查询时间复杂度为O(k)(k为哈希函数个数,通常为3-8)。

# 四、实现原理

# 4.1 底层存储:基于String的二进制安全特性

Redis的String类型底层存储的是字节序列(char[]),每个字节占8个bit位。Bitmap本质上是对这个字节序列的bit位进行直接操作。例如,当执行SETBIT key 10 1时,Redis会进行以下计算:

  1. 计算字节索引:offset / 8 = 10 / 8 = 1(即第2个字节,索引从0开始)。
  2. 计算bit位在字节中的位置:offset % 8 = 10 % 8 = 2(即该字节的第3个bit位,从0开始)。
  3. 读取该字节的原始值,通过位运算(OR)将对应bit位设为1,再写回字节序列。

若offset对应的字节不存在(即超过当前String的长度),Redis会自动扩展字节序列,填充0,直到字节索引对应的位置存在。

# 4.2 高效性保障:位运算的底层优化

Redis对位图操作的高效性,源于底层对CPU位运算指令的直接利用:

  • SETBIT/GETBIT:单个bit位的操作仅需一次字节读取+位运算+字节写入,时间复杂度O(1)。
  • BITCOUNT:采用了“查表法”优化。Redis预定义了一个256元素的数组,存储0-255每个整数的bit 1的个数(如0的个数为0,1的个数为1,2的个数为1,...,255的个数为8)。统计时,将Bitmap的每个字节作为索引查询数组,累加结果,时间复杂度O(n)(n为字节数),效率远高于逐bit位统计。
  • BITOP:直接调用CPU的位运算指令(AND/OR/XOR/NOT),对多个字节序列进行并行运算,时间复杂度O(n)(n为最长字节序列的长度)。

# 4.3 内存占用计算

Bitmap的内存占用仅与最大偏移量有关,公式为: 内存占用(字节)= ceil(max_offset / 8)

例如:

  • 最大偏移量为100(用户ID 100):ceil(100/8) = 13字节。
  • 最大偏移量为1000万:ceil(10000000/8) = 1250000字节 ≈ 1.2MB。
  • 最大偏移量为1亿:ceil(100000000/8) = 12500000字节 ≈ 12.5MB。

注意:若偏移量跳跃极大(如仅设置offset=10000000的bit位),Redis仍会占用1.2MB空间(即使其他bit位均为0),因为需要填充到最大偏移量对应的字节。因此,使用时应尽量保证偏移量连续或集中在较小范围内。

# 五、注意事项

# 5.1 偏移量的合理设计

  • 避免过大的偏移量:若偏移量达到10亿,内存占用将达到125MB(10亿/8 = 125,000,000字节),若多个此类Bitmap并存,会占用大量内存。
  • 偏移量映射:若数据标识为非整数(如用户名、UUID),需先建立映射表(如username_to_id),将非整数标识转换为连续的整数偏移量,否则无法发挥Bitmap的空间优势。

# 5.2 键的生命周期管理

Bitmap基于String类型,若不主动设置过期时间,会永久占用内存。对于时效性数据(如每日签到、实时在线状态),应通过EXPIRE命令设置合理的过期时间,避免内存泄漏。例如:

# 设置签到记录7天后过期(自动清理)
127.0.0.1:6379> EXPIRE sign:20260113 604800  # 604800秒 = 7天
1
2

# 5.3 并发操作的安全性

SETBIT命令是原子操作(Redis所有单键命令均为原子操作),无需担心并发修改冲突。例如,多个客户端同时对同一个offset执行SETBIT,Redis会保证操作的顺序性和原子性,最终结果正确。

但需注意:若业务逻辑需要“先查询再修改”(如先GETBIT判断状态,再SETBIT修改),则存在并发竞争问题,需通过Redis事务(MULTI/EXEC)或分布式锁保证原子性。例如:

# 确保用户只能签到一次(先查后改,需事务)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> GETBIT sign:20260113 100
QUEUED
127.0.0.1:6379> SETBIT sign:20260113 100 1
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 0  # 之前未签到
2) (integer) 0  # 设置成功
# 若之前已签到,GETBIT返回1,SETBIT仍会执行,但不影响结果(可在业务层判断是否需要执行SETBIT)
1
2
3
4
5
6
7
8
9
10
11

# 5.4 误判率问题(布隆过滤器场景)

基于Bitmap的布隆过滤器存在一定的误判率(即不存在的元素可能被判断为存在),误判率与Bitmap长度、哈希函数个数相关:

  • Bitmap长度越长,误判率越低。
  • 哈希函数个数越多,误判率越低,但查询效率会略有下降。

实际使用时,需根据业务允许的误判率(如1%、0.1%),通过公式计算合理的Bitmap长度和哈希函数个数,避免误判率过高影响业务。

# 5.5 不适合的场景

  • 非布尔型数据存储:Bitmap仅能存储0/1,无法存储其他类型数据(如数值、字符串)。
  • 非整数标识的数据:若数据标识无法转换为连续整数(如随机字符串),会导致偏移量过大,内存占用激增。
  • 频繁更新和删除的场景:虽然SETBIT是原子操作,但Bitmap的删除(如删除某个用户的所有签到记录)需要遍历所有相关的Bitmap,效率较低(需通过键的过期时间间接清理)。

# 六、总结

Redis Bitmap是一种基于String类型的轻量级、高效数据结构,核心优势是极高的空间效率和高效的位运算能力。它通过将布尔型数据存储在bit位中,大幅降低了内存占用,同时支持SETBIT、GETBIT、BITCOUNT、BITOP等命令,满足签到统计、在线状态、权限控制、布隆过滤器等多种场景的需求。

使用Bitmap时,需注意合理设计偏移量、管理键的生命周期、规避并发问题,同时明确其适用场景(布尔型数据、整数标识),避免在不适合的场景中强行使用。在海量布尔型数据存储和统计场景中,Bitmap是优于Hash、Set等结构的最优选择之一。

编辑 (opens new window)
#Redis Bitmap
上次更新: 2026/01/21, 19:29:14
Redis过期删除策略

← Redis过期删除策略

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式