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 不保证能查询出相关的元素。
演示代码
准备数据
/**
* 批量获取
* @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]
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]
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]
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]
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还是会阻塞。
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
[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]