序
大家都知道redis中的keys 命令不能在生产环境乱用。因为keys命令在执行时首先会对redis内存加锁,再遍历整个库中的key获取需要的库。虽然效率很高,但生产环境如果用keys命令,很有可能导致出现线上事故。因此一般都使用scan命令替代
我每次需要用scan命令时都忘记怎么写,这里记录一下笔记
SCAN命令
SCAN命令是基于游标的迭代器,通过不断调用迭代器来进行查找过程。
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 搜索"user_token*" 每次返回5个
redis > scan 0 match user_token* count 5
1) "6"
2) 1) "user_token:1000"
2) "user_token:1001"
3) "user_token:1010"
4) "user_token:2300"
5) "user_token:1389"
redis > scan 6 match user_token* count 5
1) "10"
2) 1) "user_token:3100"
2) "user_token:1201"
3) "user_token:1410"
4) "user_token:5300"
5) "user_token:3389"
|
注意项
SCAN是增量式遍历,每次迭代返回时都只是当前时刻的元素。因此有如下特性:
- 如果有一个元素, 它从遍历开始直到遍历结束期间都存在于被遍历的数据集当中, 那么 SCAN 命令总会在某次遍历中将这个元素返回给用户。
- 如果有一个元素, 它从遍历开始就已经被删除,且直到遍历结束也没有被添加回来, 那么 SCAN 命令确保不会返回这个元素。
- 同一个元素可能会被返回多次。 处理重复元素的工作交由应用程序负责, 比如说, 可以考虑将遍历返回的元素放入Set中
- 如果一个元素是在遍历过程中被添加到数据集的, 又或者是在遍历过程中从数据集中被删除的, 那么这个元素可能会被返回, 也可能不会, 这是不确定的。
因此,
- 如果针对变动量较少的key集合,SCAN命令的结果可以视为等价于keys命令的结果
- 如果针对不断变动的key集合,SCAN命令的结果可能相比于实际上这个瞬间的key集合有多或少,并且可能重复。需要在后续的程序中处理这个问题
Redistemplate使用SCAN
使用Java的Spring框架中的模块spring-data-redis,可以方便的使用封装好的类RedisTemplate进行redis操作。
Spring Boot 3以前
Spring Boot 3.3之前,Redistemplate中不支持直接使用SCAN命令。RedisTemplate是对org.springframework.data.redis.connection
这个类的封装。因此推荐使用redisTemplate直接操作connection中的scan方法进行SCAN操作
例子,注:pattern为需要扫描的key的表达式,如business_key*
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
|
// 写法1
public Set<String> scan(String pattern) {
return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> keysTmp = new HashSet<>();
Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(pattern).count(1000).build());
while (cursor.hasNext()) {
keysTmp.add(new String(cursor.next()));
}
return keysTmp;
});
}
// 写法2
public Set<String> scanKeys(String pattern) {
Set<String> keys = new HashSet<>();
this.redisTemplate.execute((RedisConnection connection) -> {
try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(pattern).build())) {
cursor.forEachRemaining(item -> {
String key = new String(item, StandardCharsets.UTF_8);
keys.add(key);
});
return null;
}
});
return keys;
}
|
注:
- 如果不使用redieTemplate.execute,一定要主动执行关闭cursor.close()。否则可能导致连接一直保持,耗尽资源
- 使用redisTemplate.execute会自动进行cursor.close()关闭操作
Spring Boot 3.3 之后
Spring Boot 3.3之后,connection.scan被标为过时方法,且RedisTemplate支持了scan操作。因此虽然旧写法不能用了,但操作scan更方便了。同样,我们还可以使用try-with-resource语法让整个写法更优雅
例子
1
2
3
4
5
6
7
|
public Set<String> scanKeys(String pattern) {
Set<String> keys = new HashSet<>();
try(Cursor<String> cursor = redisTemplate.scan(ScanOptions.scanOptions().match(pattern).count(1000).build())){
cursor.forEachRemaining(keys::add);
}
return keys;
}
|