【Jedis详解】Cluster连接创建与关闭

1. 创建连接

使用Jedis操作Redis Cluster,我们需要创建JedisCluster对象,再通过JedisCluster对象实例操作数据,代码一般如下:

// 初始化所有节点(例如6个节点)
Set jedisClusterNode = new HashSet() {{
    add(new HostAndPort("127.0.0.1", 6379));
    add(new HostAndPort("127.0.0.1", 6380));
    add(new HostAndPort("127.0.0.1", 6381));
    add(new HostAndPort("127.0.0.1", 6382));
    add(new HostAndPort("127.0.0.1", 6383));
    add(new HostAndPort("127.0.0.1", 6384));
}};
// 初始化commnon-pool连接池,并设置相关参数
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化JedisCluster
JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);

下面我们详细了解下JedisCluster的创建逻辑,其构造函数核心功能为创建connectionHandler实例,JedisClusterConnectionHandler的构造函数如下:

public JedisClusterConnectionHandler(Set nodes,
    final GenericObjectPoolConfig poolConfig, int connectionTimeout, 
    int soTimeout, String password, String clientName,
    boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
    HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
  this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
  initializeSlotsCache(nodes, connectionTimeout, soTimeout, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
}

 其中重点需要关注的是initializeSlotsCache方法,该方法主要初始化集群中槽和节点的映射关系,下面简单介绍下Redis Cluster的数据分区和路由方案。

Redis Cluster使用虚拟槽数据分区方案,每个节点维护部分槽和数据,当需要读取数据时,使用"CRC16(key) & 16383"哈希算法得到key所在的槽,Redis Cluster本身会维护节点和槽的信息。

当你使用Dummy型客户端(例如redis-cli)在某个不具备该槽的节点上执行读取指令时,Redis Cluster会返回"MOVED"重定向信息,告诉我们该槽所在节点的IP和端口信息,然后就可以到相应的节点上执行命令读取数据。该类型的客户端需要我们自行进行重定向操作,相对不方便。

而Jedis等Smart型客户端本身就维护了槽和节点的映射关系,通过"CRC16(key) & 16383"哈希算法得到key所在的槽后,就可以通过映射关系找到对应的节点,然后直接给该节点发送指令返回数据。

下面为initializeSlotsCache方法源码:

private void initializeSlotsCache(Set startNodes,
    int connectionTimeout, int soTimeout, String password, String clientName,
    boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier) {
  for (HostAndPort hostAndPort : startNodes) {
    Jedis jedis = null;
    try {
      jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout, soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
      if (password != null) {
        jedis.auth(password);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }
      cache.discoverClusterNodesAndSlots(jedis);
      break;
    } catch (JedisConnectionException e) {
      // try next nodes
    } finally {
      if (jedis != null) {
        jedis.close();
      }
    }
  }
}

需要注意的是,由于discoverClusterNodesAndSlots内使用了WriteLock写锁,因此保证了只有一个连接会初始化节点和槽的映射关系。

重点分析下discoverClusterNodesAndSlots方法:

public void discoverClusterNodesAndSlots(Jedis jedis) {
  //写锁保证只有一个连接进行初始化
  w.lock();

  try {
    //清空旧的节点和槽数据,并关闭节点连接
    reset();
    //使用"cluster slots"指令获取Cluster节点和槽的映射关系
    /**
     * 返回结构如下:
     * 127.0.0.1:6379> cluster slots
     * 1) 1) (integer) 5462    -- 起始槽
     *    2) (integer) 10922   -- 终止槽
     *    3) 1) "127.0.0.1"    -- 主节点
     *       2) (integer) 6380
     *       3) "85371dd3c2c11dbb1cd506ed028de10bb7fa2816"
     *    4) 1) "127.0.0.1"    -- 从节点
     *       2) (integer) 6383
     *       3) "01740d2eb2c9f89014e2b8b673d444753d0685cd"
     * 2) 1) (integer) 10923
     *    2) (integer) 16383
     *    3) 1) "127.0.0.1"
     *       2) (integer) 6381
     *       3) "aacad0a5a2b37d4e11f2253849263575adb78740"
     *    4) 1) "127.0.0.1"
     *       2) (integer) 6384
     *       3) "5e099198bba08597d695f6e2e3db2d6ec1494534"
     * 3) 1) (integer) 0
     *    2) (integer) 5461
     *    3) 1) "127.0.0.1"
     *       2) (integer) 6379
     *       3) "672cc8350bb1ac1d94e9c511ea234f6b7f86cab1"
     *    4) 1) "127.0.0.1"
     *       2) (integer) 6382
     *       3) "6bab67c3619c4b9cbdfd247ff3e446308a0c6326"
     */
    List slots = jedis.clusterSlots();

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

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

      //解析出所负责的的所有槽
      List slotNums = getAssignedSlotArray(slotInfo);

      // hostInfos
      /**
       *  解析主节点和从节点信息,构建槽和节点的映射关系:
       *  1. 添加nodes元素,key为":",value为该节点对应的JedisPool连接池
       *  2. 添加slots元素,key为"slotNum",value为该槽对应节点的JedisPool连接池
       */
      int size = slotInfo.size();
      for (int i = MASTER_NODE_INDEX; i < size; i++) {
        List hostInfos = (List) slotInfo.get(i);
        if (hostInfos.size() <= 0) {
          continue;
        }

        //获取节点IP端口信息
        HostAndPort targetNode = generateHostAndPort(hostInfos);
        //设置nodes,Map.Entry结构为
        //从节点和主节点都会进行保存
        setupNodeIfNotExist(targetNode);
        if (i == MASTER_NODE_INDEX) {
          //设置slots,Map.Entry结构为
          //只需要设置槽和主节点的映射关系
          assignSlotsToNode(slotNums, targetNode);
        }
      }
    }
  } finally {
    w.unlock();
  }
} 
  

维护节点和槽的映射关系主要通过JedisClusterInfoCache里面的两个Map类型的变量:

private final Map nodes = new HashMap();

private final Map slots = new HashMap();

该方法的主要流程如下:

1. 清空nodes和slots数据,并关闭关联的JedisPool。

2. 使用"cluster slots"指令获取Cluster节点和槽的映射关系,其返回结构如下:

127.0.0.1:6379> cluster slots
1) 1) (integer) 5462 .    -- 起始槽
   2) (integer) 10922 .   -- 终止槽
   3) 1) "127.0.0.1" .    -- 主节点IP
      2) (integer) 6380   -- 主节点端口
      3) "85371dd3c2c11dbb1cd506ed028de10bb7fa2816"  -- 主节点runId
   4) 1) "127.0.0.1" .    -- 从节点IP
      2) (integer) 6383   -- 从节点端口
      3) "01740d2eb2c9f89014e2b8b673d444753d0685cd"  -- 从节点runId
2) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 6381
      3) "aacad0a5a2b37d4e11f2253849263575adb78740"
   4) 1) "127.0.0.1"
      2) (integer) 6384
      3) "5e099198bba08597d695f6e2e3db2d6ec1494534"
3) 1) (integer) 0
   2) (integer) 5461
   3) 1) "127.0.0.1"
      2) (integer) 6379
      3) "672cc8350bb1ac1d94e9c511ea234f6b7f86cab1"
   4) 1) "127.0.0.1"
      2) (integer) 6382
      3) "6bab67c3619c4b9cbdfd247ff3e446308a0c6326"

3. 解析出所负责的的所有槽,代码如下:

private List getAssignedSlotArray(List slotInfo) {
  List slotNums = new ArrayList();
  for (int slot = ((Long) slotInfo.get(0)).intValue(); slot <= ((Long) slotInfo.get(1))
      .intValue(); slot++) {
    slotNums.add(slot);
  }
  return slotNums;
} 
  

slotNums的数量即为该主节点所负责的槽的数量。

4. 解析主节点和从节点信息,构建槽和节点的映射关系。

值得注意的是,nodes里面会保存主节点和从节点的信息,但是slots里面槽只对应主节点的连接池JedisPool,从节点并需要设置。

最后值得一提的是,Jedis连接Sentinel时,连接的是Sentinel集群,通过“masterName”参数获取Sentinel集群监控的主节点连接,后续的操作也是通过该主节点连接执行。但连接Cluster时,其实连接的是每个Redis实例,读取数据时会根据Jedis维护的槽和节点映射关系去对应的节点上读取。

2. 关闭连接

JedisCluster关闭代码如下:

jedisCluster.close();

其关闭源码逻辑主要调用JedisClusterInfoCache类的reset方法,该方法关闭了所有集群节点的连接,并将nodes和slots数据清空。

//BinaryJedisCluster类
@Override
public void close() {
  if (connectionHandler != null) {
    connectionHandler.close();
  }
}

//JedisClusterConnectionHandler类
@Override
public void close() {
  cache.reset();
}

//JedisClusterInfoCache类
public void reset() {
  w.lock();
  try {
    for (JedisPool pool : nodes.values()) {
      try {
        if (pool != null) {
          pool.destroy();
        }
      } catch (Exception e) {
        // pass
      }
    }
    nodes.clear();
    slots.clear();
  } finally {
    w.unlock();
  }
}

 

你可能感兴趣的:(Redis)