文档中心>云数据库 Redis®>实践教程>集群架构全局 SCAN 使用指南

集群架构全局 SCAN 使用指南

最近更新时间:2025-07-16 17:55:56

我的收藏

基本介绍

腾讯云 Redis® 集群版通过创新的 Proxy 架构解决了原生 Redis Cluster 不支持全局 SCAN 的限制,在原生 Redis Cluster 中,由于数据分散在多个分片节点,无法直接进行跨节点扫描操作;腾讯云通过扩展 SCAN 命令功能,支持两种精准扫描模式:用户可通过在命令末尾添加 NODEID 参数实现针对特定分片节点的定向扫描,同时从 Proxy 5.8.9 版本开始,全面支持无节点限制的全局 SCAN 操作,实现对集群所有分片数据的完整遍历能力。

使用限制

当集群发生节点切换(主从角色变更)、分片缩减(集群缩容)时,可能触发 SCAN 游标小概率的失效,导致游标对应的分片索引与实际节点映射关系断裂。若此时继续使用原游标执行扫描,系统将抛出错误 ,例如,-ERR invalid cursor(master node idx out of range)\\r\\n),此异常说明游标绑定的历史分片状态已失效。若强行忽略错误继续迭代,不仅无法获取有效数据,更会因游标与分片逻辑不同步进入死循环,严重消耗系统资源。

参考代码

在 Redis 集群环境下,执行 SCAN 操作时,若捕获到由集群拓扑变更引发的游标异常,需将游标重置为起始位0并重启全局扫描流程。该策略直接规避了因分片索引失效导致的游标死循环风险,确保数据遍历的完整性与安全性。如下代码实现了一个基于 Redis SCAN 命令的安全键扫描功能,特别针对大型 Redis 数据库的键遍历需求。具体连接配置,请参见 Jedis 连接 Redis
package com.example.service.impl;
import com.example.config.RedisConnectionFactory;
import com.example.service.RedisService;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.ScanParams;
import redis.clients.jedis.resps.ScanResult;
import java.util.*;
/**
* Redis服务实现类
*/
public class RedisServiceImpl implements RedisService {
//scan最大重试次数
private static final int MAX_RETRIES = 3;
private final RedisConnectionFactory connectionFactory;
/**
* 构造函数
*/
public RedisServiceImpl() {
this.connectionFactory = RedisConnectionFactory.getInstance();
}
/**
* scan最佳实践
* @param pattern 匹配的模式
* @param count 每次迭代返回的 key 的数量
*/
@Override
public void scanKeys(String pattern, int count) {
try (Jedis jedis = connectionFactory.getConnection()) {
int retryCount = 0;
boolean scanCompleted = false;
int totalKeysProcessed = 0;
int totalBatches = 0;
long startTime = System.currentTimeMillis();
// 对scan过程的报错进行捕获,遇到报错从0开始重新scan扫描
while (!scanCompleted && retryCount <= MAX_RETRIES) {
String cursor = ScanParams.SCAN_POINTER_START;
ScanParams scanParams = new ScanParams();
scanParams.count(count);
scanParams.match(pattern);
try {
while (true) {
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
List<String> keys = scanResult.getResult();
totalBatches++;
if (!keys.isEmpty()) {
totalKeysProcessed += keys.size();
// 处理获取到的键 - 业务逻辑
processKeysWithBusinessLogic(keys);
}
cursor = scanResult.getCursor();
// 游标为"0"时完成扫描
if (cursor.equals(ScanParams.SCAN_POINTER_START)) {
long endTime = System.currentTimeMillis();
double timeElapsed = (endTime - startTime) / 1000.0;
System.out.println("\\n???? 扫描完成!");
System.out.println("总批次数: " + totalBatches);
System.out.println("总键数: " + totalKeysProcessed);
System.out.println("扫描耗时: " + timeElapsed + " 秒");
System.out.println("平均速度: " + (totalKeysProcessed / timeElapsed) + " 键/秒");
scanCompleted = true;
break;
}
}
} catch (Exception e) {
retryCount++;
System.err.println("⚠️ 扫描过程中发生错误,尝试从0开始重新扫描 (" + retryCount + "/" + MAX_RETRIES + ")");
e.printStackTrace();
// 超过最大重试次数,终止重试
if (retryCount > MAX_RETRIES) {
throw new RuntimeException("达到最大重试次数 (" + MAX_RETRIES + "),扫描失败", e);
}
}
}
} catch (Exception e) {
System.err.println("扫描键失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 处理键的业务逻辑
*/
private void processKeysWithBusinessLogic(List<String> keys) {
// 业务处理逻辑....
System.out.println(keys.size());
}
}