1. 写入数据

JedisCluster设置string类型数据的set方法如下:

@Override
public String set(final String key, final String value) {
  return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
    @Override
    public String execute(Jedis connection) {
      return connection.set(key, value);
    }
  }.run(key);
}

JedisClusterCommand为抽象类,execute为抽象方法,因此需要重写该方法,核心逻辑run方法代码如下:

public T run(String key) {
  return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
}

 

(1)通过哈希算法获取key对应的slot。

public static int getSlot(String key) {
  if (key == null) {
    throw new JedisClusterOperationException("Slot calculation of null is impossible");
  }
  //提取hash_tag
  key = JedisClusterHashTagUtil.getHashTag(key);
  // optimization with modulo operator with power of 2 equivalent to getCRC16(key) % 16384
  return getCRC16(key) & (16384 - 1);
}
  • JedisClusterHashTagUtil.getHashTag(key) 用于提取hash_tag,我们简单介绍下Redis的hash_tag,key内部使用大括号包含的内容叫做hash_tag,可以提供不同的key可以具备相同slot的功能。例如,key:{user}:1 和 key:{user}:2 会映射到相同的slot。getHashTag方法在于提取大括号中的内容。
  • 使用 "CRC16(key) & 16383" 获取到对应的slot。

(2)通过槽和节点的映射关系找到对应节点连接,执行命令。

private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
  if (attempts <= 0) {
    throw new JedisClusterMaxAttemptsException("No more cluster attempts left.");
  }

  Jedis connection = null;
  try {

    /**
     * 服务端返回重定向信息,Redis有两种重定向:
     * 1. MOVED重定向:集群已经完成故障转移及数据迁移,主从节点发生切换,客户端需要更新slots缓存。
     * 2. ASK重定向:集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只是临时性的重定向,客户端不会更新slots缓存。
     */
    if (redirect != null) {
      //从重定向异常提取出目标节点信息
      connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
      //如果是ASK重定向,需要发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
      if (redirect instanceof JedisAskDataException) {
        // TODO: Pipeline asking with the original command to make it faster....
        connection.asking();
      }
    } else {
      if (tryRandomNode) {
        //随机获取某个节点的连接池,并使用"ping-pong"机制保证节点是存活态。
        connection = connectionHandler.getConnection();
      } else {
        //从slots中获取slot对应的节点连接,如果不存在则需要刷新slots映射。
        connection = connectionHandler.getConnectionFromSlot(slot);
      }
    }

    //执行命令
    return execute(connection);

  } catch (JedisNoReachableClusterNodeException jnrcne) {
    throw jnrcne;
  } catch (JedisConnectionException jce) {
    // release current connection before recursion
    releaseConnection(connection);
    connection = null;

    //当发生连接异常时,会发生重试,最后一次尝试时会刷新slots数据。
    //采用最后一次才刷新,是因为在刷新方法内部使用了写锁,导致所有请求都阻塞,对于并发量高的场景将极大地影响集群吞吐。
    if (attempts <= 1) {
      //We need this because if node is not reachable anymore - we need to finally initiate slots
      //renewing, or we can stuck with cluster state without one node in opposite case.
      //But now if maxAttempts = [1 or 2] we will do it too often.
      //TODO make tracking of successful/unsuccessful operations for node - do renewing only
      //if there were no successful responses from this node last few seconds
      this.connectionHandler.renewSlotCache();
    }

    return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
  } catch (JedisRedirectionException jre) {
    // if MOVED redirection occurred,
    //发生MOVED重定向,说明已经发生故障转移及数据迁移,即主从节点进行切换,因此需要刷新slots的映射信息。
    if (jre instanceof JedisMovedDataException) {
      // it rebuilds cluster's slot cache recommended by Redis cluster specification
      this.connectionHandler.renewSlotCache(connection);
    }

    // release current connection before recursion
    releaseConnection(connection);
    connection = null;

    return runWithRetries(slot, attempts - 1, false, jre);
  } finally {
    releaseConnection(connection);
  }
}

具体流程以及注意点已经在注释中标出,下面重点分析下其他的几个重要方法。

  • getConnection()

该方法主要是在nodes映射中随机选取某个节点的连接。

@Override
public Jedis getConnection() {
  // In antirez's redis-rb-cluster implementation,
  // getRandomConnection always return valid connection (able to
  // ping-pong)
  // or exception if all connections are invalid

  //使用Collections.shuffle方法产生随机数据
  List<JedisPool> pools = cache.getShuffledNodesPool();

  for (JedisPool pool : pools) {
    Jedis jedis = null;
    try {
      jedis = pool.getResource();

      if (jedis == null) {
        continue;
      }

      //Redis Cluster内部使用"ping-pong"机制来判断节点的存活状态
      String result = jedis.ping();

      if (result.equalsIgnoreCase("pong")) return jedis;

      jedis.close();
    } catch (JedisException ex) {
      if (jedis != null) {
        jedis.close();
      }
    }
  }

  throw new JedisNoReachableClusterNodeException("No reachable node in cluster");
}
  • getConnectionFromSlot(int slot)

该方法从slots数据中获取slot对应的连接。

@Override
public Jedis getConnectionFromSlot(int slot) {
  JedisPool connectionPool = cache.getSlotPool(slot);
  if (connectionPool != null) {
    // It can't guaranteed to get valid connection because of node
    // assignment
    return connectionPool.getResource();
  } else {
    renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
    connectionPool = cache.getSlotPool(slot);
    if (connectionPool != null) {
      return connectionPool.getResource();
    } else {
      //no choice, fallback to new connection to random node
      return getConnection();
    }
  }
}
  • renewSlotCache()

该方法调用JedisClusterInfoCache类的renewClusterSlots(Jedis jedis)方法来刷新slots映射。

public void renewSlotCache() {
  cache.renewClusterSlots(null);
}
  • renewClusterSlots(Jedis jedis)

该方法需要了解的有两点:

(1)通过volatile变量和写锁保证每次只有一个线程执行。

(2)当jedis参数不为空时,只刷新对应节点的slots和nodes映射数据;为空时则刷新所有节点的映射关系。

 

其核心逻辑是在discoverClusterSlots方法里。

private volatile boolean rediscovering;

public void renewClusterSlots(Jedis jedis) {
  //If rediscovering is already in process - no need to start one more same rediscovering, just return
  if (!rediscovering) {
    try {
      w.lock();
      if (!rediscovering) {
        rediscovering = true;

        try {
          if (jedis != null) {
            try {
              discoverClusterSlots(jedis);
              return;
            } catch (JedisException e) {
              //try nodes from all pools
            }
          }

          for (JedisPool jp : getShuffledNodesPool()) {
            Jedis j = null;
            try {
              j = jp.getResource();
              discoverClusterSlots(j);
              return;
            } catch (JedisConnectionException e) {
              // try next nodes
            } finally {
              if (j != null) {
                j.close();
              }
            }
          }
        } finally {
          rediscovering = false;      
        }
      }
    } finally {
      w.unlock();
    }
  }
}
  • discoverClusterSlots(Jedis jedis)

discoverClusterSlots方法与《Cluster连接创建与关闭》中介绍的discoverClusterNodesAndSlots方法相似,大体都是通过"cluster slots"指令获取槽和节点的信息, 更新slots和nodes数据。

private void discoverClusterSlots(Jedis jedis) {
  List<Object> slots = jedis.clusterSlots();
  this.slots.clear();

  for (Object slotInfoObj : slots) {
    List<Object> slotInfo = (List<Object>) slotInfoObj;

    if (slotInfo.size() <= MASTER_NODE_INDEX) {
      continue;
    }

    List<Integer> slotNums = getAssignedSlotArray(slotInfo);

    // hostInfos
    //获取主节点IP端口信息
    List<Object> hostInfos = (List<Object>) slotInfo.get(MASTER_NODE_INDEX);
    if (hostInfos.isEmpty()) {
      continue;
    }

    // at this time, we just use master, discard slave information
    HostAndPort targetNode = generateHostAndPort(hostInfos);
    assignSlotsToNode(slotNums, targetNode);
  }
}

public void assignSlotsToNode(List<Integer> targetSlots, HostAndPort targetNode) {
  w.lock();
  try {
    JedisPool targetPool = setupNodeIfNotExist(targetNode);
    for (Integer slot : targetSlots) {
      slots.put(slot, targetPool);
    }
  } finally {
    w.unlock();
  }
}

public JedisPool setupNodeIfNotExist(HostAndPort node) {
  w.lock();
  try {
    //ip:port格式  
    String nodeKey = getNodeKey(node);
    JedisPool existingPool = nodes.get(nodeKey);
    if (existingPool != null) return existingPool;

    JedisPool nodePool = new JedisPool(poolConfig, node.getHost(), node.getPort(),
        connectionTimeout, soTimeout, password, 0, clientName, 
        ssl, sslSocketFactory, sslParameters, hostnameVerifier);
    nodes.put(nodeKey, nodePool);
    return nodePool;
  } finally {
    w.unlock();
  }
}

2. 读取数据

JedisCluster读取string类型数据的get方法如下:

@Override
public String get(final String key) {
  return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
    @Override
    public String execute(Jedis connection) {
      return connection.get(key);
    }
  }.run(key);
}

public T run(String key) {
  return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
}

从上面代码可知,读取数据的流程是与写入数据的流程是一致的,这里就不再赘述。

但有一点值得注意的是,runWithRetries方法中的tryRandomNode参数可以决定数据是否一定在主节点上读写,set和get方法都是使用false参数,因此会调用getConnectionFromSlot方法获取连接,由于slots中存储的都是主节点的连接,因此默认都是在主节点上进行操作。

当tryRandomNode参数设置为true时,会调用getConnection方法,该方法随机从nodes中获取连接,而nodes中存储了主从节点的连接,因此有可能获取到从节点的连接。