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

摘要

BF(Bloom Filter)

BF 命令说明

命令 功能说明 是否创建 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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package com.example.redisbloom;

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


import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Component
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 void reserve(String key, double errorRate, long capacity) {
redisTemplate.execute((RedisCallback<String>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.RESERVE', KEYS[1], " + errorRate + ", " + capacity + ")").getBytes(),
ReturnType.STATUS,
1,
key.getBytes()
)
);
}

/**
* 添加元素到 BloomFilter,BloomFilter 不存在会自动创建
*/
public boolean add(String key, String value) {
// 使用 RedisModule 提供的 BF.ADD 命令
return Boolean.TRUE.equals(redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.ADD', KEYS[1], ARGV[1])").getBytes(),
ReturnType.BOOLEAN,
1,
key.getBytes(),
value.getBytes()
)
));
}

/**
* 判断元素是否存在
*/
public boolean exists(String key, String value) {
return Boolean.TRUE.equals(redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.EXISTS', KEYS[1], ARGV[1])").getBytes(),
ReturnType.BOOLEAN,
1,
key.getBytes(),
value.getBytes()
)
));
}

/**
* 批量添加
*/
public List<Boolean> addBatch(String key, String... items) {
if (items == null || items.length == 0) {
return Collections.emptyList();
}

// 构建 keys + args 数组
byte[][] keysAndArgs = new byte[1 + items.length][];
keysAndArgs[0] = key.getBytes(); // KEYS[1]
for (int i = 0; i < items.length; i++) {
keysAndArgs[i + 1] = items[i].getBytes(); // ARGV[1..n]
}

// 调用 BF.MADD 命令
return redisTemplate.execute((RedisCallback<List<Boolean>>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.MADD', KEYS[1], unpack(ARGV))").getBytes(),
ReturnType.MULTI,
1, // numKeys
keysAndArgs // KEYS + ARGV
)
);
}

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

if (items == null || items.length == 0) {
return Collections.emptyList();
}

// 构建 keys + args 数组
byte[][] keysAndArgs = new byte[6 + items.length][];
keysAndArgs[0] = key.getBytes(); // KEYS[1]
keysAndArgs[1] = "CAPACITY".getBytes();
keysAndArgs[2] = String.valueOf(capacity).getBytes();
keysAndArgs[3] = "ERROR".getBytes();
keysAndArgs[4] = String.valueOf(errorRate).getBytes();
keysAndArgs[5] = "ITEMS".getBytes();
for (int i = 0; i < items.length; i++) {
keysAndArgs[i + 6] = items[i].getBytes(); // ARGV[1..n]
}

// 调用 BF.INSERT 命令
return redisTemplate.execute((RedisCallback<List<Boolean>>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.INSERT', KEYS[1], unpack(ARGV))").getBytes(),
ReturnType.MULTI,
1, // numKeys
keysAndArgs // KEYS + ARGV
)
);
}

/**
* 批量判断元素是否存在
*/
public List<Boolean> mexists(String key, String... items) {
if (items == null || items.length == 0) {
return Collections.emptyList();
}

// 构建 keys + args 数组
byte[][] keysAndArgs = new byte[1 + items.length][];
keysAndArgs[0] = key.getBytes(); // KEYS[1]
for (int i = 0; i < items.length; i++) {
keysAndArgs[i + 1] = items[i].getBytes(); // ARGV[1..n]
}

// 调用 BF.MADD 命令
return redisTemplate.execute((RedisCallback<List<Boolean>>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.MEXISTS', KEYS[1], unpack(ARGV))").getBytes(),
ReturnType.MULTI,
1, // numKeys
keysAndArgs // KEYS + ARGV
)
);
}


/**
* 获取元素数量
*/
public Long card(String key) {
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.scriptingCommands().eval(
("return redis.call('BF.CARD', KEYS[1])").getBytes(),
ReturnType.INTEGER,
1,
key.getBytes()
)
);
}

/**
* 获取 Bloom Filter 元信息
*/
public Map<String, Object> info(String key) {
List<Object> result = redisTemplate.execute(
(RedisCallback<List<Object>>) connection ->
connection.scriptingCommands().eval(
"return redis.call('BF.INFO', KEYS[1])".getBytes(),
ReturnType.MULTI,
1,
key.getBytes()
)
);

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

Map<String, Object> infoMap = new LinkedHashMap<>();
for (int i = 0; i < result.size(); i += 2) {
String field = toString(result.get(i));
Object value = 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);
}
}