Redis Cluster集群 scan特定前缀的key 遇到的问题和解决方案

针对Redis集群环境下无法直接使用`scan`命令的问题,提出了通过键特定前缀分配到同一节点的策略,以及利用二级索引和Set结构优化数据检索。同时,介绍了使用一致性哈希进行键的分片优化,以避免单个Set过大导致的获取问题。

最近做的项目的技术需求,里面需要scan redis里面的所有key,在单机下面的话,使用一下方法是直接没问题的,附上工具代码

    public List<String> getKeysByPattern(String patternKey, Integer scanCount) {

        try {
            long start = System.currentTimeMillis();

            List<String> resultKeys = objectRedisTemplate.execute((RedisCallback<List<String>>) connection -> {
                List<String> tmpKeys = Lists.newArrayList();
                Jedis jedis = (Jedis) connection.getNativeConnection();
                ScanParams scanParams = new ScanParams();
                scanParams.match(patternKey);
                scanParams.count(scanCount);
                long start1 = System.currentTimeMillis();
                ScanResult<String> scan = jedis.scan("0", scanParams);
                log.info("patternKey:[{}],scan once time:[{}]", patternKey, System.currentTimeMillis() - start1);
                long start2 = System.currentTimeMillis();
                while (null != scan.getStringCursor()) {
                    tmpKeys.addAll(scan.getResult());
                    log.info("patternKey:[{}],scan getResult time:[{}]", patternKey, System.currentTimeMillis() - start2);
                    start2 = System.currentTimeMillis();
                    if (!StringUtils.equals("0", scan.getStringCursor())) {
                        scan = jedis.scan(scan.getStringCursor(), scanParams);
                        continue;
                    } else {
                        break;
                    }
                }
                return tmpKeys;

            });

            log.info("scan扫描共耗时:{} ms,patternKey[{}],key数量:{}", System.currentTimeMillis() - start, patternKey, resultKeys.size());
            return resultKeys;
        } catch (Exception e) {
            log.error("getKeysByPattern Exception :[{}]", e.getMessage(), e);
        }
        return null;
    }

但是如果在redis cluster集群下 会报node节点异常,原因是使用RedisTemplate这个工具没办法做协调节点,后面找了很多博客做什么每个节点遍历都不行。

后面看到有人说 可以设计 key 特定前缀的key 落在某一个节点,因为的的key设计里面有时间,所以redis的key可以这么设计

key: {xxxxxxx:202104271530}:xxx:xxx

这样的话 同一个分钟的数据就会落在同一个节点,而不同分钟的数据可以落在不同节点 ( {}的作用是使 里面内容一样的key 落在同一个节点)

不过虽然scan游标的话不会像keys一样阻塞线程,但是如果频繁的请求redis的话,会造成redis的cpu负载过大,从而拖慢服务速度,而且scan的速度不快。因此,真心不推荐用scan。

后面我就想了另一种方案解决,就是做二级索引,采用 redis 的 set结构

key:{xxxxxx:202104271530}

value:[

{xxxxxx:202104271530}:1111

{xxxxxx:202104271530}:2222
.....
{xxxxxx:202104271530}:9999

]

这样的话就不需要去scan整个redis集群,直接通过特定前缀获取所有的reids key,但是这样还是有一个问题,如果遇到所有的key的数量太过庞大 set 太大的话 在做get的时候也会有问题

这样的话 其实可以对前缀key进行分片优化,分片方法如下:

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;

import java.util.SortedMap;
import java.util.TreeMap;

public class PartitionByMurmurHashFunction {

    private static final int DEFAULT_VIRTUAL_BUCKET_TIMES = 160;
    private static final int DEFAULT_WEIGHT = 1;

    private int count;
    private int seed;
    private int virtualBucketTimes = DEFAULT_VIRTUAL_BUCKET_TIMES;

    private HashFunction hash;

    private SortedMap<Integer, Integer> bucketMap;


    public PartitionByMurmurHashFunction(int count) {
        this.count = count;
        this.bucketMap = new TreeMap<>();
        generateBucketMap();
    }

    private void generateBucketMap() {
        hash = Hashing.murmur3_32(seed);//计算一致性哈希的对象
        for (int i = 0; i < count; i++) {//构造一致性哈希环,用TreeMap表示
            StringBuilder hashName = new StringBuilder("SHARD-").append(i);
            for (int n = 0, shard = virtualBucketTimes * DEFAULT_WEIGHT; n < shard; n++) {
                bucketMap.put(hash.hashUnencodedChars(hashName.append("-NODE-").append(n)).asInt(), i);
            }
        }
    }


    public Integer calculate(String columnValue) {
        SortedMap<Integer, Integer> tail = bucketMap.tailMap(hash.hashUnencodedChars(columnValue).asInt());
        if (tail.isEmpty()) {
            return bucketMap.get(bucketMap.firstKey());
        }
        return tail.get(tail.firstKey());
    }

    public static void main(String[] args) {
        PartitionByMurmurHashFunction pp = new PartitionByMurmurHashFunction(10);
        System.out.println(pp.calculate("166666661"));
        System.out.println(pp.calculate("260266661"));
        System.out.println(pp.calculate("366788663"));
        System.out.println(pp.calculate("466666664"));
        System.out.println(pp.calculate("566125666"));
        System.out.println(pp.calculate("666900666"));

    }
}

采用该方法分片后的key:

key:{xxxxxx:202104271530}:0

value:[

{xxxxxx:202104271530}:1111

.....

]

key:{xxxxxx:202104271530}:1

value:[

{xxxxxx:202104271530}:2222
.....

]

这样就解决了

在使用 `RedisCluster`(Redis 集群模式)时,**无法直接跨节点执行 `KEYS` 或 `SCAN` 全局查找所有匹配前缀key**,因为 Redis 集群将数据分散到多个分片(哈希槽,hash slots)上。每个 key 根据其哈希值被分配到某个特定的节点。 因此,要获取带有指定前缀(如 `'aaa:'`)的所有 key,必须: 1. 遍历集群中的每一个 master 节点; 2. 在每个节点上执行 `SCAN` 命令查找匹配 `aaa:*` 的 key; 3. 合并结果。 由于 `redis-py` 官方库从 4.0+ 开始支持集群模式(通过 `redis.cluster.RedisCluster`),我们可以使用它来实现这一功能。 --- ### ✅ 正确做法:遍历所有 master 节点并 scan ```python from redis.cluster import RedisCluster # 连接 Redis 集群 r = RedisCluster(host='localhost', port=7000, decode_responses=True) # 指定前缀 prefix = 'aaa:*' results = {} # 获取所有 master 节点 for node in r.get_master_nodes(): try: # 在每个节点上执行 SCAN 迭代 for key in r.scan_iter(match=prefix, _target_nodes=node): try: value = r.get(key) # 假设是字符串类型 results[key] = value except Exception as e: results[key] = f"Error reading value: {e}" except Exception as e: print(f"Error scanning node {node}: {e}") # 输出结果 for k, v in results.items(): print(f"{k} -> {v}") ``` > 🔍 **说明**: > - `get_master_nodes()` 获取所有主节点。 > - `scan_iter(..., _target_nodes=node)` 显式指定在当前节点上扫描。 > - `decode_responses=True` 确保返回的是字符串而不是 bytes。 > - 因为 key 是根据 `{key}` 的哈希分布的,所以某些前缀key 可能只存在于部分节点上。 --- ### ⚠️ 注意事项 1. **性能问题**: - 对每个节点都进行 `SCAN` 是 O(n),大数据量下较慢。 - 建议设置 `count` 参数控制每次迭代数量,例如:`scan_iter(match=prefix, count=100, _target_nodes=node)` 2. **不支持跨节点通配查询**: - Redis Cluster 设计如此,没有全局 key 空间视图。 - 所以必须手动遍历节点。 3. **仅限于字符串?** - 上面示例用 `r.get(key)`,假设是 string 类型。 - 若有多种类型,请先判断 `r.type(key)` 再读取(参考单机版处理方式)。 4. **连接参数说明**: - `host/port` 应指向任意一个集群节点,客户端会自动发现整个拓扑。 - 必须开启集群模式支持。 --- ### ❌ 错误做法(会导致异常) ```python # 错误!不能直接 scan 所有节点 for key in r.scan_iter('aaa:*'): # 缺少 _target_nodes,可能报错或遗漏数据 ... ``` 如果没有指定 `_target_nodes`,`scan_iter` 默认只会作用于“default”节点组,在某些配置下可能无法正常工作。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值