redis-cluster主流客户端驱动不支持pipeline,该怎么解决。

redis-cluster,是基于redis的基础上开发分布式缓存数据库系统。

1、redis-cluster集群的特点:

1)无中心节点(share-nothing)架构,各个节点对等,每个节点存储集群数据的一部分。

2)集群数据分布在所有master节点上的16384个slots中,无冗余。

3)通过redis配置slave节点,完成数据备份存储以及提供failover节点。

4)redis cluster提供HA方案。节点之间通过gossip协议进行通信,交换节点的状态信息;如果超过半数以上的master节点与故障节点通信超时,则认为该节点是故障节点,则通过投票协议完成选举,实现redis 主从节点自动切换(auto-failover)。

5)直接ASK 转向/MOVED 转向机制.

6)redis cluster支持在线扩容、迁移和升级。

2、redis-cluster不足之处:

1)任意master挂掉,且当前master没有slave时,集群进入fail状态;

2)如果集群中超过半数的master故障,不管故障master是否有存活的salve,集群都会进入fail状态;

3)目前主流的redis-cluster客户端均不支持pipeline操作(这节所讲的重点)

3、如何实现redis-cluster的pipeline操作?

redis是一个cs模式的tcp-server,使用和http类似的请求响应协议,一个client可以通过一个socket链接发送多个请求命令。每次client发出请求命令后通常会阻塞等待server处理请求,server端处理完请求命令后给客户端返回响应报文。如果我们发送四次请求,需要8个tcp报文才能完成,其中如果包含报文发送时间,网络延时,这极大的浪费了redis server的处理数据的能力。具体通信过程如下:

client: set f1 1

server:OK

client: set f2 1

server:OK

client: set f3 1

server:OK

client: set f4 1

server:OK

如果我们利用redis的pipeline操作,我们则可以把多个命令打包成一个请求报文一次性发送给redis服务端,这样就不需要等待单条命令执行结果的返回,而redis服务端会处理完多条命令后会将多条命令的处理结果打包到一起返回给客户端。通信过程如下

client: set f1 1

client: set f2 1

client: set f3 1

client: set f4 1

server:OK

server:OK

server:OK

server:OK

但是目前redis-cluster客户端并不支持pipeline操作,客户端在初始化redis集群数据的时候,使用的是JedisCluster而不是Jedis对象,而JedisCluster 并没有发现提供有像Jedis一样的获取Pipeline对象的 pipelined()方法.

    private static JedisCluster jc;
    
    private final static String REDIS_CLUSTER_HOSTS = "redis_cluster_hosts";
    
    static {
        String redis_cluster_hosts = Config.getConfigValue(REDIS_CLUSTER_HOSTS);
        String[] redis_hosts = redis_cluster_hosts.split(",");
        Set jedisClusterNodes = new HashSet();
        for(String redisNode:redis_hosts){
            String[] hostPort = redisNode.split(":");
            HostAndPort hostAndPort = new HostAndPort(hostPort[0], Integer.parseInt(hostPort[1]));
            jedisClusterNodes.add(hostAndPort);
        }
        jc = new JedisCluster(jedisClusterNodes);
    }

根据redis-cluster的特性,集群数据分布在所有master节点上的16384个slots中,redis客户端在存储key的时候,首先是计算key存储的slot,再通过集群中solt与节点的映射,插入到对应的slot的master节点中。

如果我们需要支持批量操作,第一步,我们需要通过下面代码得到slotHostMap映射:

Map nodeMap = jc.getClusterNodes();
String anyHostAndPortStr = nodeMap.keySet().iterator().next();
TreeMap slotHostMap = getSlotHostMap(anyHostAndPortStr);
/**
 * 获取slot与host对应关系
 * @param anyHostAndPortStr
 * @return
 */
private static TreeMap getSlotHostMap(String anyHostAndPortStr) {
    TreeMap tree = new TreeMap();
    String parts[] = anyHostAndPortStr.split(":");
    HostAndPort anyHostAndPort = new HostAndPort(parts[0], Integer.parseInt(parts[1]));
    Jedis jedis = null;
    try{
        jedis = new Jedis(anyHostAndPort.getHost(), anyHostAndPort.getPort());
        List list = jedis.clusterSlots();
        for (Object object : list) {
            List list1 = (List) object;
            List master = (List) list1.get(2);
            String hostAndPort = new String((byte[]) master.get(0)) + ":" + master.get(1);
            tree.put((Long) list1.get(0), hostAndPort);
            tree.put((Long) list1.get(1), hostAndPort);
        }
    }catch(Exception e){
        logger.error("getSlotHostMap error",e);
    }
    return tree;
} 
  
通过上面的代码,我们可以得到每个master节点上分布的slot。这样我们就可以通过key计算出slot,再通过slot找到对应存储的节点。

  int slot = JedisClusterCRC16.getSlot(key); 
        //获取到对应的Jedis对象
        Map.Entry entry = slotHostMap.lowerEntry(Long.valueOf(slot));
        Jedis jedis = nodeMap.get(entry.getValue()).getResource();

找到了Jedis对象,那我们就可以通过jedis.pipelined()创建pipeline对象实现pipeline了。

在我们暴露给其他开发人员使用的批量操作方法中,是支持多个key批量操作,在计算每个key存储的slot之后,我们需要对通过节点上的key进行分类,然后分批通过pipeline操作同一个redis节点。

 public static String batchSet(Map keyValues){
        logger.error("keymap size={}",keyValues.size());
        //通过key获取到相应的node,再通过node把key做分类
        Map> todoMap = getNodeKeyMap(keyValues);
        try {
            //todoMap保存了node与key之间的一对多的映射关系
            for(String key:todoMap.keySet()){
                Jedis jedis = nodeMap.get(key).getResource();
                Map kv = todoMap.get(key);
                if(kv.size() > 0){
                    Pipeline p = jedis.pipelined();
                    for(String _key:kv.keySet()){
                        p.set(_key, kv.get(_key));
                    }
                    p.sync();
                }
            }
        } catch (Exception e) {
          logger.error("batchSet error",e);
        }
        return null;
    }

以上的一系列操作,最终的目的还是为了获取到单个master节点的Jedis对象,获取到pipeline对象。

休息之余总结出来的方法,如果大家有更好的方法,欢迎一起探讨。qq:1549690699


你可能感兴趣的:(java,redis,redis-cluster)