如何安全、高效地遍历 redis 中的 key
redis 是一个高性能的内存数据存储系统,它广泛应用于缓存、消息队列等场景。尽管 Redis 提供了非常快速的数据操作,但当涉及到遍历所有键(keys)时,可能会遇到性能瓶颈,尤其是在数据量很大的时候。
此时,我们可以使用 scan 命令安全、高效地遍历所有键(keys)
1. 核心方法:scan
这是最直接的方法,RedisTemplate
提供了一个 scan
方法,它返回一个 Cursor
,你可以像使用迭代器一样遍历它。
方法签名:
Cursor<K> scan(ScanOptions options);
参数:
ScanOptions
: 扫描选项,用于配置匹配模式 (PATTERN
)、每次返回的数量 (COUNT
) 等。通常使用ScanOptions.scanOptions().build()
来构建。
示例:遍历所有以 user:
开头的键
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ScanExample {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void scanKeys() {
// 定义扫描选项:匹配 user:* 的模式
ScanOptions scanOptions = ScanOptions.scanOptions()
.match("user:*") // 设置匹配模式,可为 null 匹配所有
.count(100) // 建议设置每次迭代返回的大致数量,非精确值,可优化性能
.build();
// 执行 SCAN 命令
try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
while (cursor.hasNext()) {
String key = cursor.next();
System.out.println("Found key: " + key);
// 这里可以对找到的 key 进行操作,例如:redisTemplate.opsForValue().get(key);
}
} catch (IOException e) {
// 处理异常
e.printStackTrace();
}
}
}
关键点说明:
match(“user:*”)
: 使用通配符进行模式匹配。*
代表任意多个字符,?
代表一个字符。count(100)
: 强烈建议设置。这只是一个提示给服务器的“建议值”,服务器每次返回的数量可能多于或少于这个值。设置一个合理的值(如 100-1000)可以在迭代大量键时平衡网络往返次数和单次返回的数据量,优化性能。如果不设置,Redis 会使用默认值。try-with-resources
: 使用try
语句确保Cursor
会被正确关闭,释放资源。- 非原子性:在扫描过程中,如果键被添加或删除,可能会被看到,也可能不会,这是允许的。
2. 通过 KeySet 命令与 ScanOptions
如果你只是想获取一个包含所有匹配键的 Set
,而不是逐个迭代,可以使用 keys
方法并传入 ScanOptions
。注意:对于非常大的键空间,这仍然可能消耗大量内存并阻塞服务器,因为它最终会收集所有键。
public Set<String> findKeysByPattern(String pattern) {
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.build();
// 底层使用了 SCAN 命令来收集所有结果
return redisTemplate.keys(pattern, scanOptions); // 注意:此方法可能仍在内部遍历所有键,最终组成Set返回
}
警告: 虽然底层实现可能使用了 SCAN
,但 keys(K pattern)
这个方法的名字很容易让人误解为是 KEYS
命令。在 Spring Data Redis 2.1+ 版本中,redisTemplate.keys(pattern)
方法默认已经使用 SCAN
命令来替代危险的 KEYS
命令了。但直接调用 keys(pattern)
(不带 ScanOptions
)和 keys(pattern, scanOptions)
最终效果类似,都是收集所有结果到一个 Set 中。对于海量数据,这有 OOM 风险。因此,优先使用返回 Cursor
的 scan()
方法进行增量迭代。
总结与最佳实践
特性/方法 | redisTemplate.scan(ScanOptions) | redisTemplate.keys(pattern) / keys(pattern, ScanOptions) |
---|---|---|
原理 | 基于 SCAN 命令,增量式迭代 | 内部使用 SCAN 遍历,但收集所有结果到一个集合中 |
性能 | 高。不会阻塞 Redis 服务器,内存占用小(每次只处理一批键) | 低。对于百万级键,可能消耗大量客户端内存,并长时间占用服务器 |
使用场景 | 生产环境首选。需要遍历大量键并进行操作(如批量删除、查询、迁移) | 开发、测试或键空间很小的时候。需要立即获取所有键名的集合 |
返回值 | Cursor<K> (可迭代) | Set<K> |
最佳实践:
- 永远不要在生产环境使用
KEYS
命令。相应地,尽量避免使用redisTemplate.keys(“*”)
,除非你非常确定数据量很小。 - 使用
scan(ScanOptions)
方法进行迭代,这是安全、高效的方式。 - 总是设置
COUNT
选项。根据你的网络和服务器性能调整一个合适的值(例如 500)。 - 在迭代过程中进行操作:在
cursor.next()
拿到键名后,可以紧接着执行get
、delete
等操作,实现高效的批量处理。
批量删除示例(安全做法):
public void deleteKeysByPatternSafely(String pattern) {
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(pattern)
.count(500) // 每次扫描500个
.build();
try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
List<String> keysToDelete = new ArrayList<>();
while (cursor.hasNext()) {
String key = cursor.next();
keysToDelete.add(key);
// 每积累 100 个键,批量删除一次
if (keysToDelete.size() >= 100) {
redisTemplate.delete(keysToDelete);
keysToDelete.clear();
}
}
// 删除最后一批不足100的键
if (!keysToDelete.isEmpty()) {
redisTemplate.delete(keysToDelete);
}
} catch (IOException e) {
e.printStackTrace();
}
}
这个删除示例避免了在迭代过程中频繁发起单个 DEL
命令,而是采用小批量删除的方式,在性能和内存占用上取得了很好的平衡。
作者:https://blog.xn--rpv331d.com/望舒
链接:https://blog.xn--rpv331d.com/望舒/blog/111
转载注意保留文章出处...