关于在Redis集群模式下不支持Scan和pipline的解释以及解决方案

 最近在写程序的过程中遇到一个问题,就是需要删除redis中key值以某个字符串开头的数据。因为通过查阅资料以后说如果数据量过大使用keys可能会产生死锁,并且速度会很慢,所以通过查阅各种资料发现使用scan会是比较好的一种方案,但是在真实的开发过程中发现了单节点的redis会支持scan但是集群环境中并不支持scan命令(本人使用的是redistemplate,Jedis是Redis官方推荐的面向Java的操作Redis的客户端,而RedisTemplate是SpringDataRedis中对JedisApi的高度封装。SpringDataRedis相对于Jedis来说可以方便地更换Redis的Java客户端,比Jedis多了自动管理连接池的特性,方便与其他Spring框架进行搭配使用如:SpringCache),不仅如此并且之前所熟知的Pipline管道操作也是不支持集群环境的,通过翻阅各种网上的博客以及redis的官网,自己简单的实现了一下在java中redis集群环境下scan和pipline的使用。
为什么这两种操作在redis集群环境中是不支持的:
 因为redis中有一个槽(slot)的概念,如果在单个节点的情况下redis是只有16384个槽,使用pipline只建立一次连接,如果循环操作他会首先将数据放入到缓冲区,然后一次将数据发送并且获取,这样无疑比每一次获取建立一次连接性能要高很多。但是在集集群的模式之下,多个节点共享16384个槽,假设集群有三个节点那么此时就是三个节点平分16384个槽位,当放入一个数据是会对key进行hash然后根据hash以后的值找到对应的槽位放入对应的节点,但是如果此时使用pipline就会发现问题,因为pipline要建立一次连接,但是操作的key可能在不同的分片上,所以此时就需要进行请求的转发,但是这是与pipline的思想相违背的所以集群模式下不支持pipline的操作。scan是同样的道理,不同的key可能在不同的节点上,但是scan只能操作一个节点上的数据。
 通过以上的分析那么就知道应该如何解决这个问题了,那就是对所有的key进行hash找出其对应的槽位并且分类。保证同一pipeline内所有的key都对应一个节点就好了,最后通过pipeline执行。当然scan也是同样的道理此外还有mget和mset。但是scan的解决方案就是通过获取每个节点然后通过每个节点的scan的命令来进行寻找key值。
关于pipline和scan使用的代码实现:
(1)Pipline:

public static List<Object> clusterPiplineGetAndSet(List<String> keys) throws Exception {

    //获取key的序列化策略
    RedisSerializer keySerializer = redisTemplate.getKeySerializer();
    //定义的map以redis的节点为key  list为value,此处的list存放的该节点下需要存储的key值
    HashMap<RedisClusterNode, List<String>> nodeKeyMap = new HashMap<>(8);
    List<Object> result = new ArrayList<>(8);
    //获取集群的连接对象
    RedisClusterConnection redisClusterConnection = redisTemplate.getConnectionFactory().getClusterConnection();
    try {
        //通过计算每个key的槽点,获取所有的节点
        Iterable<RedisClusterNode> redisClusterNodes = redisClusterConnection.clusterGetNodes();
        for (RedisClusterNode redisClusterNode : redisClusterNodes) {
            //得到节点的槽位的范围
            RedisClusterNode.SlotRange slotRange = redisClusterNode.getSlotRange();
            for (String key : keys) {
                //利用redis的key的hash算法得到该key对应的槽位
                int slot = JedisClusterCRC16.getSlot(key);
                if (slotRange.contains(slot)) {
                    List<String> list = nodeKeyMap.get(redisClusterNode);
                    if (null == list) {
                        list = new ArrayList<>();
                        nodeKeyMap.putIfAbsent(redisClusterNode, list);
                    }
                    //将对应的key放入进去
                    list.add(key);
                }
            }
        }
        //开始遍历通过管道往redis中放入数据 遍历上边定义的map
        for (Map.Entry<RedisClusterNode, List<String>> clusterNodeListEntry : nodeKeyMap.entrySet()) {
            //连接节点
            RedisClusterNode redisClusterNode = clusterNodeListEntry.getKey();
            //获取到每个节点的JedisPool对象  关于jedis和redistemplate的关系下边会进行简单介绍。
            JedisPool jedisPool = ((JedisCluster) redisClusterConnection.getNativeConnection()).getClusterNodes()
                    .get(new HostAndPort(redisClusterNode.getHost(), redisClusterNode.getPort()).toString());
            List<String> nodeListEntryValue = clusterNodeListEntry.getValue();
            byte[][] arr = new byte[nodeListEntryValue.size()][];
            int count = 0;
            //获取key数据
            for (String nodeKey : nodeListEntryValue) {
                //利用之前获取到的键的序列化方式对每个值进行序列化并且放入到byte数组中。
                arr[count++] = keySerializer.serialize(nodeKey);
            }
            //从池子中拿出对应的jedis对象
            Jedis jedis = jedisPool.getResource();
            List<Response<byte[]>> responses = new ArrayList<>();
            try {
                //开始使用单节点的pipline对象进行操作。
                Pipeline pipeline = jedis.pipelined();
                //************************接下来的操作就是对应利用pipline获取值和set值可以根据业务需求选取******************************
                
                //从redis中获取值
                for (String nodeKey : nodeListEntryValue) {
                    responses.add(pipeline.get(keySerializer.serialize(nodeKey)));
                }
                List<Object> objects = pipeline.syncAndReturnAll();
                result = objects;

                 //往redis中放入值
                /* for (String nodeKey : nodeListEntryValue) {
                     pipeline.set(keySerializer.serialize(nodeKey),valueSerializer.serialize("nnnn"));
                 }
                 pipeline.sync();*/
                
                //*************************************************************************************************************
                
                pipeline.close();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.close();
            }

        }
    } finally {
        RedisConnectionUtils.releaseConnection(redisClusterConnection, redisTemplate.getConnectionFactory());
    }
    return result;
}

(2)Scan:

    public static List<String> getRedisKeys(String matchKey) {

    List<String> list = new ArrayList<>();
    RedisClusterConnection redisClusterConnection = redisTemplate.getConnectionFactory().getClusterConnection();
    //这里是获取edispool的另外一种方式与上边的pipline可以对比下,两种方式都可以实现
    Map<String, JedisPool> clusterNodes = ((JedisCluster) redisClusterConnection.getNativeConnection()).getClusterNodes();
    for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
        //获取单个的jedis对象
        Jedis jedis = entry.getValue().getResource();
        // 判断非从节点(因为若主从复制,从节点会跟随主节点的变化而变化),此处要使用主节点从主节点获取数据
        if (!jedis.info("replication").contains("role:slave")) {
            List<String> keys = getScan(jedis, matchKey);
            if (keys.size() > 0) {
                Map<Integer, List<String>> map = new HashMap<>(8);
                //接下来的循环不是多余的,需要注意
                for (String key : keys) {
                    // cluster模式执行多key操作的时候,这些key必须在同一个slot上,不然会报:JedisDataException:
                    int slot = JedisClusterCRC16.getSlot(key);
                    // 按slot将key分组,相同slot的key一起提交
                    if (map.containsKey(slot)) {
                        map.get(slot).add(key);
                    } else {
                        List<String> list1 = new ArrayList();
                        list1.add(key);
                        map.put(slot, list1);
                    }
                }
                for (Map.Entry<Integer, List<String>> integerListEntry : map.entrySet()) {
                    list.addAll(integerListEntry.getValue());
                }
            }
        }
    }
    return list;
}

public static List<String> getScan(Jedis redisService, String key) {
    
    List<String> list = new ArrayList<>();
    //扫描的参数对象创建与封装
    ScanParams params = new ScanParams();
    params.match(key);
    //扫描返回一百行,这里可以根据业务需求进行修改
    params.count(100);
    String cursor = "0";
    ScanResult scanResult = redisService.scan(cursor, params);

    //scan.getStringCursor() 存在 且不是 0 的时候,一直移动游标获取
    while (null != scanResult.getStringCursor()) {
        //封装扫描的结果
        list.addAll(scanResult.getResult());
        if (! "0".equals( scanResult.getStringCursor())) {
            scanResult = redisService.scan(cursor, params);
        } else {
            break;
        }
    }
    return list;
}

以上便是pipline和scan在集群环境下的实现过程,这只是线下的测试过程具体的redistemplate还需要业务上自行实现,同时一些异常操作安全操作也需要开发者自行实现。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值