使用scan代替keys获取所有key

介绍

Rediskeys命令可以获取所有的key, 时间复杂度是O(n), 一旦数据量大了, 因为Redis是单线程的, 就会导致Redis阻塞的情况.
为了解决阻塞问题, Redis 2.8.0推出了scan命令, scan可以返回默认大小为10key, 并返回一个游标, 作为下次调用scan的参数.

使用

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
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8 k9 v9 k10 v10 k11 v11 k12 v12 k13 v13 k14 v14
127.0.0.1:6379> keys *
1) "k14"
2) "k1"
3) "k13"
4) "k5"
5) "k2"
6) "k12"
7) "k11"
8) "k10"
9) "k4"
10) "k8"
11) "k9"
12) "k3"
13) "k6"
14) "k7"
127.0.0.1:6379> scan 0 match * count 10
1) "11"
2) 1) "k5"
2) "k3"
3) "k6"
4) "k7"
5) "k1"
6) "k11"
7) "k14"
8) "k12"
9) "k2"
10) "k13"
127.0.0.1:6379> scan 11 match * count 10
1) "0"
2) 1) "k10"
2) "k4"
3) "k8"
4) "k9"

初始化一堆key

  1. keys命令获取到所有的key
  2. scan命令两次获取到所有的key

多线程下原子性问题思考

我们在上面使用了两次scan命令, 就说明在这两次scan中, 可能会发生set或者del操作, 不是一个原子性操作.

Elements that were not constantly present in the collection during a full iteration, may be returned or not: it is undefined.

根据官方文档, 也就是说, 如果在scan过程中setdel了某个key, 那么这个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
28
29
30
31
32
33
34
35
36
37
38
39
40
127.0.0.1:6379> scan 0 match * count 10
1) "11"
2) 1) "k5"
2) "k3"
3) "k6"
4) "k7"
5) "k1"
6) "k11"
7) "k14"
8) "k12"
9) "k2"
10) "k13"
127.0.0.1:6379> set k15 v15
OK
127.0.0.1:6379> scan 11 match * count 10
1) "0"
2) 1) "k10"
2) "k4"
3) "k8"
4) "k9"

127.0.0.1:6379> scan 0 match * count 10
1) "13"
2) 1) "k5"
2) "k3"
3) "k6"
4) "k7"
5) "k1"
6) "k11"
7) "k14"
8) "k15"
9) "k12"
10) "k2"
127.0.0.1:6379> scan 13 match * count 10
1) "0"
2) 1) "k13"
2) "k10"
3) "k4"
4) "k8"
5) "k9"

可以看到, k15没有在第一次扫描时返回, 而在第二次扫描时返回.
所以这个玄学状态, 应该是取决于setdel的元素的位置.

整合 Spring Data Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public void scan(String pattern, Consumer<String> consumer) {
RedisSerializer<String> keySerializer = (RedisSerializer<String>) stringRedisTemplate.getKeySerializer();

ScanOptions options = ScanOptions.scanOptions().match(pattern).count(10).build();
try(Cursor<String> cursor = getStringRedisTemplate().executeWithStickyConnection((RedisCallback<Cursor<String>>) connection ->
new ConvertingCursor<>(connection.scan(options), keySerializer::deserialize))) {

if(cursor == null) {
return;
}
while (cursor.hasNext()) {
String key = cursor.next();
consumer.accept(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

整合到Spring Data Redis中就是这样, 奇怪的是, 最新的Spring Data Redis 2.1.6实现了hscan命令, 但是却没有实现scan, 只能自己写execute实现了.
可以参考我的工具类集合RedisHelper.

参考资料