本文介绍在云分布式缓存数据库(兼容 Redis)集群架构下,如何使用扩展的 SCAN 命令遍历集群数据,并处理游标失效场景,避免扫描进入死循环。
业务场景
在批量清理过期 Key、统计 Key 分布、数据巡检或迁移前核对等运维场景中,业务需要遍历实例中的全部 Key。原生 Redis Cluster 的数据分散在多个分片节点上,不支持全局 SCAN,无法直接跨节点扫描,给上述运维操作带来不便。云分布式缓存数据库集群架构通过 Proxy 架构扩展了 SCAN 能力,支持在单条命令中完成跨分片的遍历,简化了集群下的全量 Key 遍历操作。
背景信息
云分布式缓存数据库集群架构通过 Proxy 架构扩展了 SCAN 命令,支持两种扫描模式:
定向扫描: 在命令末尾添加
NODEID 参数,仅针对指定分片节点扫描。全局扫描: 从 Proxy 5.8.9版本开始,支持无节点限制的全局 SCAN,可完整遍历集群所有分片的数据。
前提条件
已创建云分布式缓存数据库集群架构实例,并获取实例的连接地址与访问密码。
实例的 Proxy 版本不低于5.8.9(全局扫描能力所需)。
已完成客户端连接配置,连接方式可参见 Jedis 连接 Redis。
使用限制
说明:
当集群发生节点切换(主从角色变更)或分片缩减(集群缩容)时,可能小概率触发 SCAN 游标失效,导致游标对应的分片索引与实际节点映射关系断裂,报错如下:
-ERR invalid cursor(master node idx out of range)若忽略该错误继续迭代,不仅无法获取有效数据,还会因游标与分片逻辑不同步而进入死循环,持续消耗系统资源。因此扫描逻辑必须对游标失效做异常处理。
操作步骤
步骤1:定向扫描指定分片
在 SCAN 命令末尾追加
NODEID 参数,可只扫描指定分片节点的数据,适用于按分片排查或分片级处理的场景。使用示例:当集群中某个分片出现热点 Key 导致延迟升高时,可通过定向扫描该分片快速定位大 Key 或异常 Key 的分布情况,无需遍历全集群数据。假设该分片的节点 ID 为
f2f3c387b9fab0e67af02039845c60278b13bed0,执行如下命令:scan 0 MATCH * COUNT 100 f2f3c387b9fab0e67af02039****************
节点 ID 为40位十六进制字符串,可通过控制台「节点管理」页面查看,或执行
cluster nodes 命令获取。步骤2:执行全局扫描
不带
NODEID 参数时,SCAN 将遍历集群所有分片的数据(需 Proxy 5.8.9及以上)。从游标 0 开始迭代,直到游标重新返回 0 表示遍历完成。步骤3:处理游标失效并安全遍历
为规避游标失效导致的死循环,扫描逻辑应捕获异常,将游标重置为起始位0并重启扫描流程,同时设置最大重试次数。完整参考代码(Java + Jedis)如下:
package com.example.service.impl;public class RedisServiceImpl implements RedisService {// scan 最大重试次数private static final int MAX_RETRIES = 3;private final RedisConnectionFactory connectionFactory;@Overridepublic void scanKeys(String pattern, int count) {try (Jedis jedis = connectionFactory.getConnection()) {int retryCount = 0;boolean scanCompleted = false;// 对 scan 过程报错进行捕获,遇到报错从 0 开始重新扫描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();if (!keys.isEmpty()) {processKeysWithBusinessLogic(keys); // 业务处理}cursor = scanResult.getCursor();// 游标为 "0" 时完成扫描if (cursor.equals(ScanParams.SCAN_POINTER_START)) {scanCompleted = true;break;}}} catch (Exception e) {retryCount++;// 超过最大重试次数,终止重试if (retryCount > MAX_RETRIES) {throw new RuntimeException("达到最大重试次数,扫描失败", e);}}}}}}
关键参数与逻辑说明如下:
参数 / 逻辑 | 说明 |
MAX_RETRIES | SCAN 最大重试次数,示例为3,可按业务需要调整。 |
cursor | 游标,每轮扫描从起始位 0(ScanParams.SCAN_POINTER_START)开始,游标重新返回 0 表示遍历完成。 |
scanParams.match(pattern) | 按模式匹配 Key。 |
scanParams.count(count) | 每次迭代返回的元素数量提示。 |
异常捕获 | 捕获到游标失效等异常后,重试计数加1并从游标 0 重新扫描;超过 MAX_RETRIES 则抛出异常终止,避免死循环。 |
验证方法
观察扫描过程:游标最终重新返回
0 且 scanCompleted 为 true,表示已完整遍历,未陷入死循环。统计扫描结果:记录处理的总批次数、总 Key 数量与耗时,与预期数据规模核对,确认遍历完整。
常见问题
Q:扫描时报错
invalid cursor(master node idx out of range) 怎么办?该报错通常由集群节点切换或缩容导致游标失效引起。应捕获异常后将游标重置为
0 并重新发起扫描,切勿忽略错误继续用原游标迭代,否则会进入死循环。Q:如何避免全局 SCAN 陷入死循环?
必须对扫描过程做异常处理,并设置最大重试次数:捕获到游标异常时从
0 重启扫描,超过重试上限则终止并抛出异常。Q:只想扫描某个分片的数据怎么做?
使用定向扫描,在 SCAN 命令末尾追加
NODEID 参数即可,具体命令写法以实例实际支持为准。