分布式缓存技术之Redis_Redis集群连接及底层源码分析


**本次源码分析基于: jedis-3.0.1 **

1. Jedis 单点连接

  当是单点服务时,Java 连接Redis的客户端:

  Jedis jedis = null;

        try {
           jedis = new Jedis("192.168.237.130", 6379);
           jedis.hset("hashzz", "k1", "v1");
        } catch (Exception e) {
            System.out.println(e);
            e.printStackTrace();
        } finally {
            if (null != jedis) {
                jedis.disconnect();
            }
        }

 或者

        JedisPool pool = null;
        try {
            pool = new JedisPool("192.168.237.130", 6379);
            pool.getResource().hset("hashzz", "k2", "v2");
        } catch (Exception e) {
            System.out.println(e);
            e.printStackTrace();
        } finally {
            if (null != pool) {
                pool.close();
            }
        }

2. Jedis 基于sentinel连接

基本使用

 当为了避免单点故障而使用集群环境,哨兵sentinel会对master进行监听并在master无效时进行重新选举,此种情况不能在Java中直接指定master的IP,port,Jedis提供了sentinel的方式进行连接:

 // sentinel 哨兵
        // sentinel.conf 中配置的master名称
        String masterName = "mymaster";
        // sentinel 集群环境
        Set<String> sentinelIps = new HashSet<>();
        sentinelIps.add("192.168.237.129:26370");
        sentinelIps.add("192.168.237.130:26370");
        JedisSentinelPool sentinelPool = null;

        Jedis jedis = null;

        try {
            sentinelPool = new JedisSentinelPool(masterName, sentinelIps);
            jedis = sentinelPool.getResource();

            for (int i = 0; i< 10; i++) {
                jedis.lpush("javaRedisClientList" + jedis.getClient().getHost(), new Integer(i).toString());
            }

        } catch (Exception e) {
            System.out.println(e);
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.disconnect();
            }
            if (sentinelPool != null) {
                sentinelPool.close();
            }
        }

源码分析

  • 首先查看JedisSentinelPool的构造方法,最终都会进入如下构造方法
  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
      final String password, final int database, final String clientName) {
    this.poolConfig = poolConfig;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;
    // 初始化sentinel监听列表, 并返回当前master节点
    HostAndPort master = initSentinels(sentinels, masterName);
    // 初始化
    initPool(master);
  }
  • initSentinels()方法
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");
    // 首先遍历sentinel哨兵节点 
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);

      log.debug("Connecting to Sentinel {}", hap);

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap);
        // 从当前哨兵节点获取当前master
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;
        
        if (masterAddr == null || masterAddr.size() != 2) {
          log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap);
          continue;
        }

        master = toHostAndPort(masterAddr);
        log.debug("Found Redis master at {}", master);
        // 找到master,跳出循环
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warn(
          "Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap,
          e.toString());
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }

    if (master == null) {
      // 从哨兵列表中不能获取master 
      if (sentinelAvailable) {
        // 哨兵是可用的,则可能是master有问题
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
       //  所有哨兵可能都宕机不可用
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }
   
    // 进行到这里,master和sentinel都是可用的
    log.info("Redis master running at " + master + ", starting Sentinel listeners...");

    for (String sentinel : sentinels) {
     // 为每个sentinel启动一个后台线程监听
      final HostAndPort hap = HostAndPort.parseString(sentinel);
      MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
      // whether MasterListener threads are alive or not, process can be stopped
      masterListener.setDaemon(true);
      masterListeners.add(masterListener);
      masterListener.start();
    }

    return master;
  }
  • 内部类 MasterListener
 protected class MasterListener extends Thread {

    protected String masterName;
    protected String host;
    protected int port;
    protected long subscribeRetryWaitTimeMillis = 5000;
    protected volatile Jedis j;
    protected AtomicBoolean running = new AtomicBoolean(false);

    protected MasterListener() {
    }

    public MasterListener(String masterName, String host, int port) {
      super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
      this.masterName = masterName;
      this.host = host;
      this.port = port;
    }

    public MasterListener(String masterName, String host, int port,
        long subscribeRetryWaitTimeMillis) {
      this(masterName, host, port);
      this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;
    }

    @Override
    public void run() {

      running.set(true);

      while (running.get()) {

        j = new Jedis(host, port);

        try {
          // double check that it is not being shutdown
          if (!running.get()) {
            break;
          }
          
          /*
           * Added code for active refresh
           */
          // 根据哨兵sentinel连接获取当前master
          List<String> masterAddr = j.sentinelGetMasterAddrByName(masterName);  
          if (masterAddr == null || masterAddr.size() != 2) {
            log.warn("Can not get master addr, master name: {}. Sentinel: {}:{}.",masterName,host,port);
          }else{
              initPool(toHostAndPort(masterAddr)); 
          }
          // 基于redis 频道发布订阅(channel pub/sub)实现的java内部master选举的监听
          j.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
              log.debug("Sentinel {}:{} published: {}.", host, port, message);
              // 订阅的频道发来消息
              String[] switchMasterMsg = message.split(" ");

              if (switchMasterMsg.length > 3) {

                if (masterName.equals(switchMasterMsg[0])) {
                  // 收到的消息 第一节数据 是 sentinel.conf 内配置的 master名称
                  // 根据订阅到的新的master信息, 重新初始化 master
                  initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                } else {
                  log.debug(
                    "Ignoring message on +switch-master for master name {}, our master name is {}",
                    switchMasterMsg[0], masterName);
                }

              } else {
                log.error(
                  "Invalid message received on Sentinel {}:{} on channel +switch-master: {}", host,
                  port, message);
              }
            }
          // 订阅选举频道 :  +switch-master  
          }, "+switch-master");

        } catch (JedisException e) {

          if (running.get()) {
            log.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying.", host,
              port, e);
            try {
              Thread.sleep(subscribeRetryWaitTimeMillis);
            } catch (InterruptedException e1) {
              log.error("Sleep interrupted: ", e1);
            }
          } else {
            log.debug("Unsubscribing from Sentinel at {}:{}", host, port);
          }
        } finally {
          j.close();
        }
      }
    }

    public void shutdown() {
      try {
        log.debug("Shutting down listener on {}:{}", host, port);
        running.set(false);
        // This isn't good, the Jedis object is not thread safe
        if (j != null) {
          j.disconnect();
        }
      } catch (Exception e) {
        log.error("Caught exception while shutting down: ", e);
      }
    }
  }

 可见Java Jedis连接Redis集群是基于哨兵集群的监听,首先传入哨兵的地址,根据哨兵信息及哨兵的内部通信得到当前的master连接redis客户端,然后后台为每个哨兵分配线程,线程内基于redis channel pub/sub来设立监听,如果有新的master选举,java内部订阅到消息之后重新对master进行初始化。

posted @ 2019-04-22 22:26  BigShen  阅读(572)  评论(0编辑  收藏  举报