胖胖的枫叶
主页
博客
产品设计
企业架构
全栈开发
效率工具
数据分析
项目管理
方法论
面试
  • 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年

    • 配置Mac环境
    • 业务知识会计管理
    • 业务知识会计基础
    • 业务知识什么是财务
  • 2023年

    • 项目 Boi
  • 2022年

    • 企业架构故障管理
    • 企业架构开发债务
  • 2021年

    • Python3.8 Matplotlib员工数据分析
    • Python3.8 Matplotlib IP折线图
    • Python3.8 词云 IP地址
    • Redis RediSearch
    • Rust第一个CLI程序
    • Rust所有权
    • Rust函数与控制流
    • Rust变量与数据类型
    • Rust入门
    • 企业架构分布式系统
    • 编程式权限设计
    • Java JVM优化
    • SpringBoot MyBatis 批量
    • SpringBoot 测试Mock
    • SpringBoot Redis布隆过滤器
    • CentOS7 Jenkins 部署
    • SpringBoot WebClient
    • Docker Drone 部署
    • SpringBoot MyBatis
    • SpringBoot Redisson
    • SpringBoot MyBatis 雪花算法
    • Java Netty
    • Redis 扫描
    • CentOS7 Jenkins本地部署分级
    • Mac 安装 Neo4j Jupyter
    • Mac OpenJDK11 JavaFX 环境
    • Mac 安装 Jenv
    • SpringBoot Redis 延时队列
    • SpringBoot MDC日志
    • SpringBoot 定时任务
    • CentOS7 Nginx GoAccess
    • SpringBoot MyBatis 分析
    • SpringBoot Lucene
    • 企业架构分布式锁
    • 学习技巧减少学习排斥心理
    • SpringBoot 动态数据源
    • Docker Compose SpringBoot MySQL Redis
    • SpringBoot 阻塞队列
    • Docker Compose Redis 哨兵
    • Docker Compose Redis 主从
    • 网络通信
  • 2020年

    • SpringBoot 延时队列
    • MySQL基础(四)
    • Java 雪花算法
    • Redis Geo
    • 网络通信 Tcpdump
    • Spring SPI
    • Java Zookeeper
    • SpringBoot JMH
    • 网络通信 Wireshark
    • Docker Compose Redis MySQL
    • CentOS7 Docker 部署
    • Netty 源码环境搭建
    • MySQL基础(三)
    • CentOS7 Selenium运行环境
    • CentOS7 Nginx HTTPS
    • Java JMH
    • SpringBoot 修改Tomcat版本
    • Java Eureka 钉钉通知
    • SpringBoot 错误钉钉通知
    • Java JVM
    • Git 合并提交
    • CentOS7 OpenResty 部署
  • 2019年

    • Redis CLI
    • CentOS7 Nginx 日志
    • 编程式代码风格
    • IDEA 插件
    • Skywalking 源码环境搭建
    • SpringBoot Redis 超时错误
    • 编程式 gRPC
    • Java Arthas
    • Docker Compose Redis 缓存击穿
    • Docker ElasticSearch5.6.8 部署
    • Docker Mysql5.7 部署
    • Spring Redis 字符串
    • Docker Zookeeper 部署
    • Docker Redis 部署
    • SpringBoot Dubbo
    • CentOS7 CMake 部署
    • 应用程序性能指标
    • Java Code 递归
    • CentOS7 ELK 部署
    • CentOS7 Sonarqube 部署
    • Java Selenium
    • Java JJWT JUnit4
    • Spring 源码环境搭建
    • Java JUnit4
    • Java Web JSON Token
    • 编程式 FastDFS
    • Java XPath
    • Redis基础(二)
    • Redis基础(一)
    • Java MyBatis JUnit4
    • Java MyBatis H2 JUnit4
    • MyBatis 源码环境搭建
    • Git 配置
    • Java 核心
    • Java Dubbo
    • Java JavaCollecionsFramework
    • Java Maven
    • Java MyBatis
    • Java Spring
    • Java SpringMVC
    • MySQL
    • Redis
  • 2018年

    • Java HashMap
    • Java HashSet
    • Java Code 交换值
    • Spring Upgrade SpringBoot
    • Mac 编程环境
    • Java Log4j
    • 网络通信 Modbus
    • MySQL基础(二)
    • MySQL基础(一)
    • Java Stack
    • Java Vector
    • CentOS7 RabbitMQ 部署
    • CentOS7 Redis 部署
    • CentOS7 MongoDB 部署
    • CentOS7 基础命令
    • Java Eureka Zookeeper
    • CentOS7 MySQL 部署
    • Git 分支
    • CentOS7 Java环境配置
    • Java LinkedList
    • Java ArrayList
    • Spring Annotation Aop

Redis 扫描

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

最近更新: 2025/12/27 18:51
Contributors: 庆峰
Prev
Java Netty
Next
CentOS7 Jenkins本地部署分级