如何安全、高效地遍历 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
转载注意保留文章出处...
