基本介绍
腾讯云 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 的数量*/@Overridepublic 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());}}