0%

Redis-Scan

Scan

游标迭代器

大于 2.8.0 版本可用。

时间复杂度:每次调用 O(1)。O(N) 用于完整的迭代,包括足够的命令调用以使光标返回 0。N 是集合内的元素数。

  • 在 Redis 2.8 之前,我们只能使用 keys 命令来查询我们想要的数据,但这个命令存在两个缺点:

  • 此命令没有分页功能,我们只能一次性查询出所有符合条件的 key 值,如果查询结果非常巨大,那么得到的输出信息也会非常多。

  • keys 命令是遍历查询,因此它的查询时间复杂度是 o(n),所以数据量越大查询时间就越长。

  • Scan:用于检索当前数据库中所有数据。

  • HScan:用于检索哈希类型的数据。

  • SScan:用于检索集合类型中的数据。

  • ZScan:由于检索有序集合中的数据。

特性

  • 它可以完整返回开始到结束检索集合中出现的所有元素,也就是在整个查询过程中如果这些元素没有被删除,且符合检索条件,则一定会被查询出来;
  • Scan 可以实现 keys 的匹配功能;
  • Scan 是通过游标进行查询的不会导致 Redis 假死;
  • Scan 提供了 count 参数,可以规定遍历的数量,但是返回并不是按照规定来的;
  • Scan 会把游标返回给客户端,用户客户端继续遍历查询;
  • Scan 返回的结果可能会有重复数据,需要客户端去重;
  • 单次返回空值且游标不为 0,说明遍历还没结束;
  • Scan 可以保证在开始检索之前,被删除的元素一定不会被查询出来;
  • 在迭代过程中如果有元素被修改, 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
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
/**
* 批量获取
* @param keys
* @return
*/
public Map<String, String> batchGet(List<String> keys) {
Map<String, String> saveMap = new HashMap<>();
redisTemplate.executePipelined(
(RedisCallback<String>) connection -> {
RedisSerializer<String> stringRedisSerializer
= RedisSerializer.string();
for (String key : keys) {
Object o = connection.get(stringRedisSerializer.serialize(key));
if (null != o) {
saveMap.put(key, String.valueOf(o));
}
}
return null;
});
return saveMap;
}

/**
* string 批量写入
* @param saveMap
* @param unit
* @param timeout
*/
public void batchInsert(Map<String, String> saveMap, TimeUnit unit, int timeout) {
/* 插入多条数据 */
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
Iterator<Map.Entry<String, String>> iterator = saveMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
redisTemplate.opsForValue().set(entry.getKey(), entry.getValue(), timeout, unit);
}
return null;
}
});
}

/**
* set 批量写入
* @param key
* @param list
* @return
*/
public Boolean sAdd(String key, List<String> list) {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer
= RedisSerializer.string();
byte[][] dataArr = new byte[list.size()][];
int count = list.size();
for (int i = 0; i < count; i++) {
dataArr[i] = stringRedisSerializer.serialize(list.get(i));
}
connection.sAdd(stringRedisSerializer.serialize(key), dataArr);
return true;
}
);
}

/**
* hash批量写入
* @param key
* @param fieldMap
* @return
*/
public Boolean hMSet(String key, Map fieldMap) {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer
= RedisSerializer.string();
Iterator<Map.Entry<String, Object>> iterator = fieldMap.entrySet().iterator();
String mapKey;
Object value;
Map<byte[], byte[]> hashes = new LinkedHashMap(fieldMap.size());
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
mapKey = entry.getKey();
value = entry.getValue();
hashes.put(stringRedisSerializer.serialize(mapKey),
stringRedisSerializer.serialize(value.toString()));
}
connection.hMSet(stringRedisSerializer.serialize(key), hashes);
return true;
}
);
}

/**
* zset 批量写入
* @param key
* @param fieldMap
* @return
*/
public Boolean zMSet(String key, Map<String, Long> fieldMap) {
return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisSerializer<String> stringRedisSerializer
= RedisSerializer.string();
Iterator<Map.Entry<String, Long>> iterator = fieldMap.entrySet().iterator();
String value;
Long score;
Set<RedisZSetCommands.Tuple> tuples = new HashSet<>();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
value = entry.getKey();
score = entry.getValue();
tuples.add(new DefaultTuple(stringRedisSerializer.serialize(value), score.doubleValue()));
}
connection.zAdd(stringRedisSerializer.serialize(key), tuples);
return true;
}
);
}

@BeforeEach
public void init() {
int count = 100;
Map<String, String> saveMap = new HashMap<>(1000);
Map<String, Long> save = new HashMap<>(1000);
List<String> keys = new ArrayList<>();
for (int i = 0; i < count; i++) {
String key = "user:" + i;
keys.add(key);
saveMap.put(key, String.valueOf(i));
save.put(key, Long.valueOf(i));
}
batchInsert(saveMap, TimeUnit.MINUTES, 10);
hMSet("mUser", saveMap);
zMSet("zUser", save);
sAdd("sUser", saveMap.values().stream().collect(Collectors.toList()));
}
  • 一共插入100个string类型。
  • 一个set集合100个元素。
  • 一个hash集合100个元素。
  • 一个zset集合100个元素。

SCAN

  • scan cursor [MATCH pattern] [COUNT count]
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
127.0.0.1:6379> scan 0 match user:* count 5
1) "16"
2) 1) "user:85"
2) "user:67"
3) "user:73"
4) "user:81"
5) "user:3"
127.0.0.1:6379> scan 5 match user:* count 5
1) "29"
2) 1) "user:46"
2) "user:52"
3) "user:60"
4) "user:30"
5) "user:93"
6) "user:16"
127.0.0.1:6379> scan 200 match user:* count 5
1) "56"
2) 1) "user:77"
2) "user:58"
3) "user:36"
4) "user:28"
5) "user:43"
127.0.0.1:6379> scan 0 match user:* count 1
1) "32"
2) 1) "user:85"
127.0.0.1:6379> scan 100 match user:* count 1
1) "20"
2) 1) "user:71"
2) "user:72"
  • cursor:光标位置,整数值,从 0 开始,到 0 结束,查询结果是空,但游标值不为 0,表示遍历还没结束;

  • match pattern:正则匹配字段;

  • count:限定服务器单次遍历的字典槽位数量(约等于),只是对增量式迭代命令的一种提示(hint),并不是查询结果返回的最大数量,它的默认值是 10。

  • 测试结果和预期不太一样,即便设置count为1,但结果不固定。ount 只是限定服务器单次遍历的字典槽位数量(约等于),而不是规定返回结果的 count 值。

HSCAN

  • hscan key cursor [MATCH pattern] [COUNT count]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> HSCAN mUser 0 match user* count 1
1) "0"
2) 1) "user:94"
2) "94"
3) "user:95"
4) "95"
5) "user:92"
6) "92"
7) "user:93"
8) "93"
9) "user:98"
10) "98"
11) "user:99"
# 返回了所有的元素

SSCAN

  • sscan key cursor [MATCH pattern] [COUNT count]
1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> sscan sUser 0 match 1* count 1
1) "0"
2) 1) "1"
2) "10"
3) "11"
4) "12"
5) "13"
6) "14"
7) "15"
8) "16"
9) "17"
10) "18"
11) "19"

ZSCAN

  • zscan key cursor [MATCH pattern] [COUNT count]
1
2
3
4
5
6
7
8
9
127.0.0.1:6379> zscan zUser 0 match u* count 10
1) "0"
2) 1) "user:0"
2) "0"
3) "user:1"
4) "1"
5) "user:2"
6) "2"
# 返回了所有的元素

测试代码

  • 实现代码发现没有游标,如果查询的数据太多还是redis还是会阻塞。
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
package cn.z201.redis;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.*;


/**
* @author z201.coding@gmail.com
**/
@Slf4j
@Component
public class ScanTool {

@Autowired
private RedisTemplate redisTemplate;

/**
* SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新的游标,
* 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
* 当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。
*
* @param pattern
* @param count
* @return
*/
public Set<String> scan(String pattern, Integer count) {
Set<String> keys = new HashSet<>();
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.count(count)
.build();
Cursor<byte[]> cursor = connection.scan(scanOptions);
while (cursor.hasNext()) {
keys.add(new String(cursor.next()));
}
return keys;
}

/**
* {@link = http://redisdoc.com/database/scan.html#hscan}
* <p>
* 可用版本: >= 2.8.0
* 时间复杂度:增量式迭代命令每次执行的复杂度为 O(1) , 对数据集进行一次完整迭代的复杂度为 O(N) , 其中 N 为数据集中的元素数量。
*
* @param key
* @param pattern
* @param count
* @return
*/
public Map<String,String> hScan(String key,String pattern, Integer count) {
Map<String,String> map = new HashMap<>();
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.count(count)
.build();
Cursor<Map.Entry<byte[], byte[]>> cursor = connection.hScan(stringRedisSerializer.serialize(key),scanOptions);
while (cursor.hasNext()) {
Map.Entry<byte[], byte[]> data = cursor.next();
if (null != data) {
map.put(stringRedisSerializer.deserialize(data.getKey()),stringRedisSerializer.deserialize(data.getValue()));
}
}
return map;
}

/**
* {@link = http://redisdoc.com/database/scan.html#sscan}
* <p>
* 可用版本: >= 2.8.0
* 时间复杂度:增量式迭代命令每次执行的复杂度为 O(1) , 对数据集进行一次完整迭代的复杂度为 O(N) , 其中 N 为数据集中的元素数量。
*
* @param key
* @param pattern
* @param count
* @return
*/
public Set<String> sScan(String key, String pattern, Integer count) {
Set<String> set = new HashSet<>();
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.count(count)
.build();
Cursor<byte[]> cursor = connection.sScan(stringRedisSerializer.serialize(key),scanOptions);
while (cursor.hasNext()) {
byte[] data = cursor.next();
if (null != data) {
set.add(stringRedisSerializer.deserialize(data));
}
}
return set;
}

/**
* {@link = http://redisdoc.com/database/scan.html#zscan}
* <p>
* 可用版本: >= 2.8.0
* 时间复杂度:增量式迭代命令每次执行的复杂度为 O(1) , 对数据集进行一次完整迭代的复杂度为 O(N) , 其中 N 为数据集中的元素数量。
*
* @param key
* @param pattern
* @param count
* @return
*/
public Map<String,Long> zScan(String key, String pattern, Integer count) {
Map<String,Long> map = new HashMap<>();
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.count(count)
.build();
Cursor<RedisZSetCommands.Tuple> cursor = connection.zScan(stringRedisSerializer.serialize(key),scanOptions);
while (cursor.hasNext()) {
RedisZSetCommands.Tuple data = cursor.next();
if (null != data) {
map.put(stringRedisSerializer.deserialize(data.getValue()),data.getScore().longValue());
}
}
return map;
}

}

@Test
@Disabled
public void test() {
Set<String> keys = scanTool.scan("user:*", 10);
log.info("{}", keys.size());
log.info("{}", keys);
Map<String, String> userMap = scanTool.hScan("mUser", "user:1*", 10);
log.info("{}", userMap.size());
log.info("{}", userMap);
Map<String, Long> userZMap = scanTool.zScan("zUser", "user:1*", 10);
log.info("{}", userZMap.size());
log.info("{}", userZMap);
Set<String> userSMap = scanTool.sScan("sUser", "1*", 10);
log.info("{}", userSMap.size());
log.info("{}", userSMap);
}
  • Console
1
2
3
4
5
6
7
8
[main]  100
[main] [user:14, user:15, user:12, user:13, user:18, user:19, user:16, user:17, user:94, user:95, user:92, user:93, user:10, user:98, user:11, user:99, user:96, user:97, user:90, user:91, user:89, user:83, user:84, user:81, user:82, user:87, user:88, user:85, user:86, user:80, user:36, user:37, user:34, user:35, user:38, user:39, user:32, user:33, user:30, user:31, user:9, user:8, user:7, user:2, user:25, user:26, user:1, user:0, user:23, user:24, user:6, user:29, user:5, user:27, user:4, user:3, user:28, user:21, user:22, user:20, user:58, user:59, user:56, user:57, user:50, user:51, user:54, user:55, user:52, user:53, user:47, user:48, user:45, user:46, user:49, user:40, user:43, user:44, user:41, user:42, user:78, user:79, user:72, user:73, user:70, user:71, user:76, user:77, user:74, user:75, user:69, user:67, user:68, user:61, user:62, user:60, user:65, user:66, user:63, user:64]
[main] 11
[main] {user:10=10, user:11=11, user:14=14, user:1=1, user:15=15, user:12=12, user:13=13, user:18=18, user:19=19, user:16=16, user:17=17}
[main] 11
[main] {user:10=10, user:11=11, user:14=14, user:1=1, user:15=15, user:12=12, user:13=13, user:18=18, user:19=19, user:16=16, user:17=17}
[main] 11
[main] [11, 1, 12, 13, 14, 15, 16, 17, 18, 19, 10]

END