Redis 命令及数据类型 -- BF(Bloom Filter)

摘要

Bloom Filter(布隆过滤器)

  • 布隆过滤器是一种用于快速判断元素是否存在的 probabilistic data structure(概率数据结构),非常适合海量数据且不要求绝对精确的场景。

  • 维基百科对 Bloom Filter 的描述

  • 生产环境推荐使用 Redisson的布隆过滤器

  • 布隆过滤器使用一个很长的二进制位数组和一系列哈希函数来保存元素。

    • 优点: 非常节省空间、查询快
    • 缺点: 有一定的误判概率、无法删除元素、无法给元素计数
  • 布隆过滤器判断一个元素不在集合中,那么这个元素肯定不在集合中。但是,布隆过滤器判断一个元素在集合中,那么这个元素有可能不在集合中。

  • 位数组(Bit Array):布隆过滤器使用一个长度固定的位数组来存储数据。每个位置只占用一个比特(0或1),初始时所有位都设置为0。位数组的长度和哈希函数的数量决定了过滤器的误报率和容量。

  • 哈希函数集合:布隆过滤器使用多个哈希函数,每个函数都会将输入数据映射到位数组的一个不同位置。哈希函数的选择对过滤器的性能有很大影响,理想的哈希函数应该具有良好的散列性,使得不同的输入尽可能均匀地映射到位数组的不同位置。

  • 如何降低误判率?

    • 更大的位数组
    • 更多的哈希函数
  • 很多人都是用过Google开源的Guava的布隆过滤器,但其是JVM层的布隆过滤器,若需要分布式布隆过滤器,就可以使用RedisBloom提供的BF(Bloom Filter)。

BF 命令说明

  • 对应Redis命令: BF.xxx

命令 功能说明 是否创建 Filter 关键参数含义 返回值 示例 使用要点 / 备注
BF.RESERVE 显式创建 Bloom Filter 是(已存在则报错) error_rate:误判率
capacity:预计元素数
EXPANSION:扩容倍率
OK BF.RESERVE user:bf 0.001 1000000 生产推荐
显式规划容量与误判率,避免隐式创建
BF.ADD 添加单个元素 是(不存在则创建) 1 新增
0 可能已存在
BF.ADD user:bf user_1 Key 必须已存在,否则报错
BF.MADD 批量添加元素 是(不存在则创建) item...:多个元素 0/1 列表 BF.MADD user:bf u1 u2 u3 ⚠ 使用默认配置,不建议生产
BF.INSERT 批量插入(可控参数) CAPACITY:容量
ERROR:误判率
NOCREATE:不自动创建过滤器
NONSCALING: 不扩容,达到capacity时,过滤器返回错误
EXPANSION expansion:扩容时,新建子过滤器的容量增长倍率,默认2
ITEMS:元素列表
0/1 列表 BF.INSERT user:bf CAPACITY 10000 ERROR 0.001 ITEMS u1 u2 最推荐的写入方式
支持初始化 + 插入
BF.EXISTS 判断单个元素是否存在 item:待判断元素 1 可能存在
0 一定不存在
BF.EXISTS user:bf user_1 不存在结果 绝对可靠
BF.MEXISTS 批量判断是否存在 item...:多个元素 0/1 列表 BF.MEXISTS user:bf u1 u9 高并发批量查询首选
BF.CARD 返回插入元素数量(近似) 整数 BF.CARD user:bf 用于容量监控,非精确
BF.INFO 返回 Bloom Filter 元信息 KV 列表 BF.INFO user:bf 运维、容量与内存分析必备
BF.SCANDUMP 分块导出 Bloom Filter iterator:游标 iterator + data BF.SCANDUMP user:bf 0 用于迁移、备份
BF.LOADCHUNK 从 dump 数据恢复 Filter iterator
data
OK BF.LOADCHUNK user:bf 1 "xxx" 必须与 SCANDUMP 配合使用

BF 命令示例

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
# 初始化一个BloomFilter,错误率0.01,元素数量1000
127.0.0.1:6379> BF.RESERVE test 0.01 1000
OK
# 查看类型
127.0.0.1:6379> type test
MBbloom--

# 添加元素
127.0.0.1:6379> BF.ADD test user1
(integer) 1
127.0.0.1:6379> BF.ADD test user2
(integer) 1
# 批量添加元素
127.0.0.1:6379> BF.MADD test user3 user4 user5
1) (integer) 1
2) (integer) 1
3) (integer) 1
# 返回Bloom过滤器的基数,即添加的元素数量(存在误差)
127.0.0.1:6379> BF.CARD test
(integer) 5
# 查询元素是否存在
127.0.0.1:6379> BF.EXISTS test user2
(integer) 1
127.0.0.1:6379> BF.EXISTS test user6
(integer) 0
# 批量查询元素是否存在
127.0.0.1:6379> BF.MEXISTS test user1 user2 user6
1) (integer) 1
2) (integer) 1
3) (integer) 0

# 获取信息
127.0.0.1:6379> BF.INFO test
1) Capacity # 初始化时的容量,超过该值后,会触发 扩容(新建一个子 Bloom Filter)
2) (integer) 1000
3) Size # 当前 Bloom Filter 实际使用的 bit array 大小(字节),由 capacity + error_rate 计算得出,值一旦创建 不会随元素减少
4) (integer) 1480
5) Number of filters # 当前 key 内部包含的 Bloom Filter 数量
6) (integer) 1
7) Number of items inserted # 已调用 BF.ADD / BF.MADD 插入的元素总数
8) (integer) 5
9) Expansion rate # Bloom Filter 扩容时,新建子过滤器的容量增长倍率
10) (integer) 2

SpringBoot 集成

  • SpringBoot 的 RedisTemplate 中没有提供对RedisBloom的封装,需要自己封装,我这里封装了一个简易的RedisBloomFilterTool

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.example.redisbloom;

/**
* 基于 RedisBloom 插件的 BloomFilter 实现
* https://github.com/RedisBloom/RedisBloom/releases
* <p>
* 不想安装插件也可以使用 Redission 的 BloomFilter
*/


import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
@Slf4j
public class RedisBloomFilterTool {

private final StringRedisTemplate redisTemplate;

public RedisBloomFilterTool(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

/**
* 初始化 BloomFilter
* <p>
* 不能重复创建
*
* @param key BloomFilter 名称
* @param errorRate 错误率,比如为0.01,即 1%
* @param capacity 容量,比如为1000
*/
public boolean reserve(String key, double errorRate, long capacity) {
String script = "return redis.call('BF.RESERVE', KEYS[1], " + errorRate + ", " + capacity + ")";
try {
redisTemplate.execute(
new DefaultRedisScript<>(script, String.class),
Collections.singletonList(key)
);
return true;
} catch (Exception e) {
log.error("RedisBloomFilterTool reserve error:", e);
return false;
}
}

/**
* 添加元素到 BloomFilter,BloomFilter 不存在会自动创建
*/
public boolean add(String key, String value) {
// 使用 RedisModule 提供的 BF.ADD 命令
String script = "return redis.call('BF.ADD', KEYS[1], ARGV[1])";
final Boolean execute = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(key),
value
);
return Boolean.TRUE.equals(execute);
}

/**
* 判断元素是否存在
*/
public boolean exists(String key, String value) {
String script = "return redis.call('BF.EXISTS', KEYS[1], ARGV[1])";
final Boolean execute = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList(key),
value
);
return Boolean.TRUE.equals(execute);
}

/**
* 批量添加
* @return 添加结果列表,成功 1,失败 0
*/
public List<Long> addBatch(String key, String... items) {
String script = "return redis.call('BF.MADD', KEYS[1], unpack(ARGV))";
if (items == null || items.length == 0) {
return Collections.emptyList();
}
// 调用 BF.MADD 命令
return redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(key),
items
);
}

/**
* 批量添加,如果BloomFilter不存在,则根据参数创建 BloomFilter,若已存在,则忽略 capacity 和 errorRate
*
* @param key BloomFilter 名称
* @param capacity 容量
* @param errorRate 错误率
* @param items 要添加的元素
* @return 添加结果列表,成功 1,失败 0
*/
public List<Long> insert(String key, long capacity, double errorRate, String... items) {

String script = "return redis.call('BF.INSERT', KEYS[1], 'CAPACITY', ARGV[1], 'ERROR', ARGV[2], 'ITEMS', unpack(ARGV, 3))";
if (items == null || items.length == 0) {
return Collections.emptyList();
}

List<Object> args = new ArrayList<>();
args.add(String.valueOf(capacity));
args.add(String.valueOf(errorRate));
Collections.addAll(args, items); // ✅ 关键点

// 调用 BF.INSERT 命令
return redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(key),
args.toArray()
);
}

/**
* 批量判断元素是否存在
* @return 存在 1,不存在 0
*/
public List<Long> mexists(String key, String... items) {

String script = "return redis.call('BF.MEXISTS', KEYS[1], unpack(ARGV))";
if (items == null || items.length == 0) {
return Collections.emptyList();
}

// 调用 BF.MADD 命令
return redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(key),
items
);
}


/**
* 获取元素数量
*/
public Long card(String key) {
String script = "return redis.call('BF.CARD', KEYS[1])";
// 使用 RedisModule 提供的 BF.CARD 命令
return redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key)
);
}


/**
* 获取 Bloom Filter 元信息
*/
public Map<String, Long> info(String key) {
String script = "return redis.call('BF.INFO', KEYS[1])";

List<Object> result = redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(key)
);

if (result == null || result.isEmpty()) {
return Collections.emptyMap();
}

Map<String, Long> infoMap = new LinkedHashMap<>();
for (int i = 0; i < result.size(); i += 2) {
String field = toString(result.get(i));
Long value = (Long) result.get(i + 1);
infoMap.put(field, value);
}

return infoMap;
}

private String toString(Object obj) {
if (obj instanceof byte[]) {
return new String((byte[]) obj);
}
return String.valueOf(obj);
}
}

SpringBoot 与 Redisson 集成之 BloomFilter 的使用方法

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.52.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
@Autowired
private RedissonClient redissonClient;

……………………………………………………………………………………………………………………………………………………………………………………………………………………
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("bloomFilter");
bloomFilter.tryInit(1000000, 0.01); // 初始化
bloomFilter.add("test"); // 添加元素
System.out.println(bloomFilter.contains("test")); // 判断是否存在
System.out.println(bloomFilter.count()); // 获取当前布隆过滤器中已添加的元素个数
System.out.println(bloomFilter.getSize()); // 获取当前布隆过滤器占用内存的大小,单位bit