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<>();
}
同理可封装 SMEMBERS
、HGETALL
、KEYS
等方法。
4.3. 问题分析
❌ 原始方式的问题:
- 时间复杂度 O(n),数据量大时性能差
- Redis 是单线程模型,
KEYS
或SMEMBERS
可能阻塞其他关键请求 - 不适合在生产环境使用,仅用于调试
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. 总结
- ✅
KEYS
、SMEMBERS
等命令简单但危险,仅用于调试 - ✅ 大数据量下必须使用
SCAN
系列命令进行分批遍历 - ✅ Jedis 提供了
ScanResult
和ScanParams
支持游标机制 - ✅ 封装
RedisIterator
可统一遍历逻辑,提升代码复用性
生产环境务必避免一次性拉取大量数据,否则可能引发 Redis 阻塞,影响整个系统稳定性。
完整代码示例已上传至 GitHub:https://github.com/yourname/redis-scan-demo