Jedis源码解析(二):JedisCluster模块源码解析

三、JedisCluster模块源码解析

1、JedisCluster类结构

在这里插入图片描述

  1. 由于Jedis本身不是线程安全的,所以选择使用对象池JedisPool来保证线程安全
  2. 在JedisClusterInfoCache中,保存了节点和槽位的一一对应关系,为每个节点建立一个对象JedisPool,并保存在map中。这个类主要用于保存集群的配置信息

2、JedisCluster的初始化

public class JedisCluster extends BinaryJedisCluster implements JedisClusterCommands,
    MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
      
  public JedisCluster(Set<HostAndPort> nodes) {
    this(nodes, DEFAULT_TIMEOUT);
  }
      
  public JedisCluster(Set<HostAndPort> nodes, int timeout) {
    this(nodes, timeout, DEFAULT_MAX_ATTEMPTS);
  }

  public JedisCluster(Set<HostAndPort> nodes, int timeout, int maxAttempts) {
    this(nodes, timeout, maxAttempts, new GenericObjectPoolConfig<Jedis>());
  }
      
  public JedisCluster(Set<HostAndPort> jedisClusterNode, int timeout, int maxAttempts,
      final GenericObjectPoolConfig<Jedis> poolConfig) {
    super(jedisClusterNode, timeout, maxAttempts, poolConfig);
  }      

JedisCluster的构造函数会调用父类BinaryJedisCluster的构造函数

public class BinaryJedisCluster implements BinaryJedisClusterCommands,
    MultiKeyBinaryJedisClusterCommands, JedisClusterBinaryScriptingCommands, Closeable {
  
  // 连接超时或读取超时,默认2秒
  public static final int DEFAULT_TIMEOUT = 2000;
  // 当JedisCluster连接失败时的重试次数,默认5次
  public static final int DEFAULT_MAX_ATTEMPTS = 5;
      
  protected JedisClusterConnectionHandler connectionHandler;    
      
  public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int timeout, int maxAttempts,
      final GenericObjectPoolConfig<Jedis> poolConfig) {
    this(jedisClusterNode, timeout, timeout, maxAttempts, poolConfig);
  }
      
  public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout,
      int soTimeout, int maxAttempts, String user, String password, String clientName,
      GenericObjectPoolConfig<Jedis> poolConfig) {
    this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
        connectionTimeout, soTimeout, user, password, clientName);
    this.maxAttempts = maxAttempts;
    this.maxTotalRetriesDuration = Duration.ofMillis((long) soTimeout * maxAttempts);
  }      

BinaryJedisCluster会调用JedisSlotBasedConnectionHandler的构造函数初始化JedisClusterConnectionHandler

public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
  
  public JedisSlotBasedConnectionHandler(Set<HostAndPort> nodes,
      GenericObjectPoolConfig<Jedis> poolConfig, int connectionTimeout, int soTimeout, String user,
      String password, String clientName) {
    super(nodes, poolConfig, connectionTimeout, soTimeout, user, password, clientName);
  }

JedisSlotBasedConnectionHandler的构造函数会调用父类JedisClusterConnectionHandler的构造函数

public abstract class JedisClusterConnectionHandler implements Closeable {

  protected final JedisClusterInfoCache cache;

  public JedisClusterConnectionHandler(Set<HostAndPort> nodes,
      final GenericObjectPoolConfig<Jedis> poolConfig, int connectionTimeout, int soTimeout,
      String user, String password, String clientName) {
    this(nodes, poolConfig, connectionTimeout, soTimeout, 0, user, password, clientName);
  }

  public JedisClusterConnectionHandler(Set<HostAndPort> nodes,
      final GenericObjectPoolConfig<Jedis> poolConfig, final JedisClientConfig clientConfig) {
    this.cache = new JedisClusterInfoCache(poolConfig, clientConfig);
    // 保存集群的节点信息和对应的槽位信息
    initializeSlotsCache(nodes, clientConfig);
  }

  private void initializeSlotsCache(Set<HostAndPort> startNodes, JedisClientConfig clientConfig) {
    ArrayList<HostAndPort> startNodeList = new ArrayList<>(startNodes);
    Collections.shuffle(startNodeList);

    for (HostAndPort hostAndPort : startNodeList) {
      try (Jedis jedis = new Jedis(hostAndPort, clientConfig)) {
        // 将集群信息保存到JedisClusterInfoCache
        cache.discoverClusterNodesAndSlots(jedis);
        return;
      } catch (JedisConnectionException e) {
        // try next nodes
      }
    }
  }

JedisClusterConnectionHandler的构造函数中会调用initializeSlotsCache()方法来保存集群的节点信息和对应的槽位信息,最终调用JedisClusterInfoCache的discoverClusterNodesAndSlots()方法将集群信息保存到JedisClusterInfoCache

public class JedisClusterInfoCache {
  
  // 集群每个节点的IP地址和对应的Pool
  private final Map<String, JedisPool> nodes = new HashMap<>();
  // 每个槽位和对应节点
  private final Map<Integer, JedisPool> slots = new HashMap<>();
  
  public void discoverClusterNodesAndSlots(Jedis jedis) {
    w.lock();

    try {
      // 清空两个map
      reset();
      // 1)通过cluster slots命令获取集群所有节点信息
      List<Object> slots = jedis.clusterSlots();
      // 逐个记录每个节点信息
      for (Object slotInfoObj : slots) {
        List<Object> slotInfo = (List<Object>) slotInfoObj;

        if (slotInfo.size() <= MASTER_NODE_INDEX) {
          continue;
        }
        // 记录当前节点的槽位信息,返回记录槽位的List
        List<Integer> slotNums = getAssignedSlotArray(slotInfo);

        // hostInfos
        int size = slotInfo.size();
        for (int i = MASTER_NODE_INDEX; i < size; i++) {
          // 检查当前节点master/slave的信息是否完整
          List<Object> hostInfos = (List<Object>) slotInfo.get(i);
          if (hostInfos.isEmpty()) {
            continue;
          }
          // 获取当前节点的HostAndPort
          HostAndPort targetNode = generateHostAndPort(hostInfos);
          // 为该节点创建Pool,塞入nodes Map
          setupNodeIfNotExist(targetNode);
          if (i == MASTER_NODE_INDEX) {
            // 将master节点和槽位信息塞入slots Map
            assignSlotsToNode(slotNums, targetNode);
          }
        }
      }
    } finally {
      w.unlock();
    }
  }

代码1)处通过cluster slots命令获取集群所有节点信息,cluster slots命令示例如下:

redis 127.0.0.1:6379> cluster slots
1) 1) (integer) 0 // 槽位开始位
   2) (integer) 4095 // 槽位结束位
   3) 1) "127.0.0.1" // 主节点IP
      2) (integer) 7000 // 主节点端口号
   4) 1) "127.0.0.1" // 从节点IP
      2) (integer) 7004 // 从节点端口号
2) 1) (integer) 12288
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7003
   4) 1) "127.0.0.1"
      2) (integer) 7007
3) 1) (integer) 4096
   2) (integer) 8191
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005
4) 1) (integer) 8192
   2) (integer) 12287
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7006

JedisCluster初始化时,通过其中一个节点从Redis服务器拿到整个集群的信息信息,包括槽位对应关系、主从节点的信息,保存在JedisClusterInfoCache中

在这里插入图片描述

3、MOVED错误和ASK错误

在讲解JedisCluster的调用流程之前,先来看下Redis集群模式的MOVED错误和ASK错误,JedisCluster会针对这两个错误进行对应处理

1)、MOVED错误

当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向正在负责槽的节点

MOVED错误的格式为:

MOVED <slot> <ip>:<port>

其中slot为键所在的槽,而ip和port则是负责处理槽slot的节点的IP地址和端口号。例如错误:

MOVED 10086 127.0.0.1:7002

表示槽10086正由IP地址为127.0.0.1,端口7002的节点负责

2)、ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令
  • 如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能会已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令

在这里插入图片描述

举个例子,假设在节点7002向节点7003迁移槽16198期间,有一个客户端向节点7002发送命令:GET "love"

因为键"love"正好属于槽16198,所以节点7002会首先在自己的数据库中查找键"love",但并没有找到,节点7002发现自己正在将槽16198迁移至节点7003,于是它向客户端返回错误:ASK 16198 127.0.0.1:7003

这个错误表示客户端可以尝试到IP为127.0.0.1,端口号为7003的节点去执行和槽16198有关的操作

在这里插入图片描述

接到ASK错误地客户端会根据错误提示的IP地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后重新发送原来想要执行的命令

在这里插入图片描述

3)、ASKING命令

ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识

如果客户端向节点发送一个关于槽i的命令,而槽i有没有指派给这个节点的话,那么节点将向客户端发回一个MOVED错误;但是,如果节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次

在这里插入图片描述

当客户端接收到ASK错误并转向至正在导入槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令,这是因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回MOVED错误

4)、MOVED错误和ASK错误的区别

MOVED错误和ASK错误都会导致客户端转向,它们的区别在于:

  • MOVED错误代表槽的负责权已经从一个节点转向到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点
  • ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现

4、JedisCluster的调用流程

public class JedisCluster extends BinaryJedisCluster implements JedisClusterCommands,
    MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
      
  @Override
  public String get(final String key) {
    return new JedisClusterCommand<String>(connectionHandler, maxAttempts, maxTotalRetriesDuration) {
      @Override
      // 模板模式,针对不同命令,有不同实现
      public String execute(Jedis connection) {
        return connection.get(key);
      }
    }.run(key);
  }

JedisClusterCommand使用了模板模式,子类重写execute()方法,然后调用run()方法

public abstract class JedisClusterCommand<T> {
  
  private final JedisClusterConnectionHandler connectionHandler;
  
  public abstract T execute(Jedis connection);

  public T run(String key) {
    // 计算该key对应的槽位
    return runWithRetries(JedisClusterCRC16.getSlot(key));
  }
  
  private T runWithRetries(final int slot) {
    Instant deadline = Instant.now().plus(maxTotalRetriesDuration);

    JedisRedirectionException redirect = null;
    int consecutiveConnectionFailures = 0;
    Exception lastException = null;
    // 最多重试maxAttempts次,重试次数用完了,抛出异常
    for (int attemptsLeft = this.maxAttempts; attemptsLeft > 0; attemptsLeft--) {
      Jedis connection = null;
      try {
        if (redirect != null) {
          // 根据回复信息重建连接
          connection = connectionHandler.getConnectionFromNode(redirect.getTargetNode());
          if (redirect instanceof JedisAskDataException) {
            // 如果是ASK错误,发送ASKING命令
            connection.asking();
          }
        } else {
          // 计算出这个key对应的槽位,根据槽位获得对应的连接
          connection = connectionHandler.getConnectionFromSlot(slot);
        }
        // 调用jedis节点执行具体命令
        return execute(connection);

      } catch (JedisConnectionException jce) {
        lastException = jce;
        ++consecutiveConnectionFailures;
        LOG.debug("Failed connecting to Redis: {}", connection, jce);
        // 重置集群信息
        boolean reset = handleConnectionProblem(attemptsLeft - 1, consecutiveConnectionFailures, deadline);
        if (reset) {
          consecutiveConnectionFailures = 0;
          redirect = null;
        }
      } catch (JedisRedirectionException jre) {
        if (lastException == null || lastException instanceof JedisRedirectionException) {
          lastException = jre;
        }
        LOG.debug("Redirected by server to {}", jre.getTargetNode());
        consecutiveConnectionFailures = 0;
        // 对JedisRedirectionException进行赋值,下次重试访问jre.getTargetNode()
        redirect = jre;
        // 如果发现MOVED ERR,说明cache保存的集群信息有错,需要重置集群信息
        if (jre instanceof JedisMovedDataException) {
          this.connectionHandler.renewSlotCache(connection);
        }
      } finally {
        // 无论此次请求发送成功或失败,都要释放该连接
        releaseConnection(connection);
      }
      if (Instant.now().isAfter(deadline)) {
        throw new JedisClusterOperationException("Cluster retry deadline exceeded.");
      }
    }

    JedisClusterMaxAttemptsException maxAttemptsException
        = new JedisClusterMaxAttemptsException("No more cluster attempts left.");
    maxAttemptsException.addSuppressed(lastException);
    throw maxAttemptsException;
  }

JedisClusterCommand的run()执行流程如下:

  1. 计算该key对应的槽位,然后调用runWithRetries()方法

  2. runWithRetries()中有个循环,最多重试maxAttempts次,重试次数用完了,抛出异常

  3. 循环里判断JedisRedirectionException是否为空,第一次执行时JedisRedirectionException为空,所以根据槽位获得对应的连接,调用对应节点执行具体命令

  4. 如果抛出JedisConnectionException,会重置集群信息

  5. 如果抛出JedisRedirectionException,JedisRedirectionException有两个子类JedisAskDataException和JedisMovedDataException,分别对应上面讲解的ASK错误和MOVED错误,这两个错误都会导致客户端的请求转向另一个节点,所以这里会对JedisRedirectionException进行赋值,下次重试访问不再访问槽位对应的节点,而直接访问jre.getTargetNode()中的节点

  6. 针对JedisMovedDataException,说明cache保存的集群信息有错,需要重置集群信息

  7. 针对JedisAskDataException,重试时会先发送ASKING命令

  8. 最后无论此次请求发送成功或失败,都要释放该连接

参考

Jedis源码分析(三)-JedisCluster的内部实现

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值