1. 概述

集合(Collection)是现代应用中最常见的数据组织方式之一。Redis 为此提供了丰富的数据结构支持,✅ 包括列表(List)、集合(Set)、哈希(Hash)、有序集合(Sorted Set)等,几乎覆盖了日常开发中的所有场景。

本文将重点讲解:如何高效、安全地获取 Redis 中所有匹配特定模式的 Key。你会看到从“简单粗暴”到“生产级”的完整演进过程,避免在高并发场景下踩坑。


2. 数据模型设计

假设我们的应用使用 Redis 存储不同运动项目中使用的球类信息。目标是能方便地查看每种球的重量数据。为简化示例,我们只考虑三种球:

  • 板球(Cricket Ball):160 克
  • 足球(Football):450 克
  • 排球(Volleyball):270 克

我们将分别用 Redis 的不同数据结构存储这些数据,并探讨各自的遍历方式。


3. 原始方式:使用 redis-cli 操作

在写 Java 代码前,先通过 redis-cli 熟悉底层命令。假设 Redis 运行在 127.0.0.1:6379

3.1. 列表(List)

使用 RPUSH 将数据存入名为 balls 的列表:

% redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> RPUSH balls "cricket_160"
(integer) 1
127.0.0.1:6379> RPUSH balls "football_450"
(integer) 2
127.0.0.1:6379> RPUSH balls "volleyball_270"
(integer) 3

✅ 插入成功后返回的是列表当前长度。可通过 LLEN 查看长度:

127.0.0.1:6379> LLEN balls
(integer) 3

使用 LRANGE 获取全部数据:

127.0.0.1:6379> LRANGE balls 0 2
1) "cricket_160"
2) "football_450"
3) "volleyball_270"

3.2. 集合(Set)

使用 SADD 存入集合,自动去重:

127.0.0.1:6379> SADD balls "cricket_160" "football_450" "volleyball_270" "cricket_160"
(integer) 3

⚠️ 虽然输入了重复值,但集合只保留唯一元素。

使用 SMEMBERS 获取所有成员:

127.0.0.1:6379> SMEMBERS balls
1) "volleyball_270"
2) "cricket_160"
3) "football_450"

3.3. 哈希(Hash)

用哈希结构存储,字段为运动名,值为重量:

127.0.0.1:6379> HMSET balls cricket 160 football 450 volleyball 270
OK

使用 HGETALL 查看全部字段:

127.0.0.1:6379> HGETALL balls
1) "cricket"
2) "160"
3) "football"
4) "450"
5) "volleyball"
6) "270"

3.4. 有序集合(Sorted Set)

以球重作为 score,运动名为 member:

127.0.0.1:6379> ZADD balls 160 cricket 450 football 270 volleyball
(integer) 3

先用 ZCARD 查长度,再用 ZRANGE 获取成员:

127.0.0.1:6379> ZCARD balls
(integer) 3
127.0.0.1:6379> ZRANGE balls 0 2
1) "cricket"
2) "volleyball"
3) "football"

3.5. 字符串(String)+ Key 模式

将每个球作为独立 Key 存储,并加前缀:

127.0.0.1:6379> MSET balls:cricket 160 balls:football 450 balls:volleyball 270
OK

使用 KEYS 命令通过模式匹配查找:

127.0.0.1:6379> KEYS balls*
1) "balls:cricket"
2) "balls:volleyball"
3) "balls:football"

⚠️ 注意:KEYS 在大数据量下会阻塞 Redis,仅限调试使用。


4. Java 原始实现(不推荐用于生产)

4.1. Maven 依赖

使用 Jedis 作为 Redis 客户端:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>5.0.2</version>
</dependency>

4.2. Redis 客户端封装

Jedis 实例不是线程安全的,应使用 JedisPool 管理连接池。同时,客户端建议使用单例模式。

private static JedisPool jedisPool;

private RedisClient(String ip, int port) {
    try {
        if (jedisPool == null) {
            jedisPool = new JedisPool(new URI("http://" + ip + ":" + port));
        }
    } catch (URISyntaxException e) {
        log.error("Malformed server address", e);
    }
}

单例获取方法(双重检查锁):

private static volatile RedisClient instance = null;

public static RedisClient getInstance(String ip, final int port) {
    if (instance == null) {
        synchronized (RedisClient.class) {
            if (instance == null) {
                instance = new RedisClient(ip, port);
            }
        }
    }
    return instance;
}

封装 LRANGE 示例:

public List<String> lrange(final String key, final long start, final long stop) {
    try (Jedis jedis = jedisPool.getResource()) {
        return jedis.lrange(key, start, stop);
    } catch (Exception ex) {
        log.error("Exception caught in lrange", ex);
    }
    return new LinkedList<>();
}

同理可封装 SMEMBERSHGETALLKEYS 等方法。

4.3. 问题分析

❌ 原始方式的问题:

  • 时间复杂度 O(n),数据量大时性能差
  • Redis 是单线程模型,KEYSSMEMBERS 可能阻塞其他关键请求
  • 不适合在生产环境使用,仅用于调试

5. 迭代器思想:分批处理

原始方式是一次性拉取所有数据,相当于“一口气读完 1000 页的书”,显然不现实。

✅ 正确做法是“分页阅读”,每次读一部分,通过“书签”记住位置。Redis 的 Scan 系列命令正是为此设计。


6. Redis Scan 命令详解

Scan 使用游标(cursor)机制,每次返回一批结果和下一个游标,直到游标为 0 表示结束。

6.1. Scan 策略

命令 用途
SCAN 遍历所有 Key
SSCAN 遍历 Set 成员
HSCAN 遍历 Hash 的 field-value 对
ZSCAN 遍历 Sorted Set 的 member-score 对

⚠️ List 没有专用 Scan 命令,但可通过 LINDEX 或分段 LRANGE 实现。

使用 SCAN 遍历 balls* 前缀的 Key:

127.0.0.1:6379> SCAN 0 MATCH balls* COUNT 1
1) "2"
2) 1) "balls:cricket"
127.0.0.1:6379> SCAN 2 MATCH balls* COUNT 1
1) "3"
2) 1) "balls:volleyball"
127.0.0.1:6379> SCAN 3 MATCH balls* COUNT 1
1) "0"
2) 1) "balls:football"

✅ 当返回游标为 "0" 时,表示遍历完成。


7. Java 中实现 Scan 迭代器

7.1. Scan 策略接口设计

Jedis 提供了对应的 Scan 方法:

public ScanResult<String> scan(final String cursor, final ScanParams params);
public ScanResult<String> sscan(final String key, final String cursor, final ScanParams params);
public ScanResult<Map.Entry<String, String>> hscan(final String key, final String cursor, final ScanParams params);
public ScanResult<Tuple> zscan(final String key, final String cursor, final ScanParams params);

ScanParams 用于设置匹配模式和每次返回数量:

ScanParams params = new ScanParams();
params.match("balls*");
params.count(10);

定义统一策略接口:

public interface ScanStrategy<T> {
    ScanResult<T> scan(Jedis jedis, String cursor, ScanParams scanParams);
}

实现各策略类(以 Hscan 为例):

public class Hscan implements ScanStrategy<Map.Entry<String, String>> {
    private String key;

    public Hscan(String key) {
        this.key = key;
    }

    @Override
    public ScanResult<Entry<String, String>> scan(Jedis jedis, String cursor, ScanParams scanParams) {
        return jedis.hscan(key, cursor, scanParams);
    }
}

7.2. RedisIterator 实现

封装迭代器,实现 Iterator<List<T>> 接口:

public class RedisIterator<T> implements Iterator<List<T>> {
    private final JedisPool jedisPool;
    private ScanParams scanParams;
    private String cursor;
    private ScanStrategy<T> strategy;

    public RedisIterator(JedisPool jedisPool, int count, String pattern, ScanStrategy<T> strategy) {
        this.jedisPool = jedisPool;
        this.strategy = strategy;
        this.scanParams = new ScanParams().match(pattern).count(count);
        this.cursor = "0";
    }

    @Override
    public boolean hasNext() {
        return !"0".equals(cursor);
    }

    @Override
    public List<T> next() {
        try (Jedis jedis = jedisPool.getResource()) {
            ScanResult<T> scanResult = strategy.scan(jedis, cursor, scanParams);
            cursor = scanResult.getCursor();
            return scanResult.getResult();
        } catch (Exception ex) {
            log.error("Exception in next()", ex);
        }
        return new LinkedList<>();
    }
}

RedisClient 中提供创建方法:

public <T> RedisIterator<T> iterator(int count, String pattern, ScanStrategy<T> strategy) {
    return new RedisIterator<>(jedisPool, count, pattern, strategy);
}

7.3. 使用示例

测试 HSCAN 策略:

@Test
public void testHscanStrategy() {
    HashMap<String, String> hash = new HashMap<>();
    hash.put("cricket", "160");
    hash.put("football", "450");
    hash.put("volleyball", "270");
    redisClient.hmset("balls", hash);

    Hscan scanStrategy = new Hscan("balls");
    RedisIterator<Map.Entry<String, String>> iterator = redisClient.iterator(2, "*", scanStrategy);

    List<Map.Entry<String, String>> results = new LinkedList<>();
    while (iterator.hasNext()) {
        results.addAll(iterator.next());
    }

    Assert.assertEquals(hash.size(), results.size());
}

✅ 该方式可安全用于生产环境,避免阻塞。


8. 总结

  • KEYSSMEMBERS 等命令简单但危险,仅用于调试
  • ✅ 大数据量下必须使用 SCAN 系列命令进行分批遍历
  • ✅ Jedis 提供了 ScanResultScanParams 支持游标机制
  • ✅ 封装 RedisIterator 可统一遍历逻辑,提升代码复用性

生产环境务必避免一次性拉取大量数据,否则可能引发 Redis 阻塞,影响整个系统稳定性。

完整代码示例已上传至 GitHub:https://github.com/yourname/redis-scan-demo


原始标题:List All Available Redis Keys

« 上一篇: Jsoniter 介绍