胖胖的枫叶
主页
博客
知识图谱
产品设计
数据分析
企业架构
项目管理
效率工具
全栈开发
后端
前端
测试
运维
数据
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 浮生若梦
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
主页
博客
知识图谱
产品设计
数据分析
企业架构
项目管理
效率工具
全栈开发
后端
前端
测试
运维
数据
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 浮生若梦
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
  • 博客

    • 博客迁移说明
    • 2024年
    • 2023年
    • 2022年
    • 2021年
    • 2020年
    • 2019年
    • 2018年

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]

END

Last Updated:
Contributors: 庆峰