Bootstrap

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

最近做的项目的技术需求,里面需要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
.....

]

这样就解决了

;