1. 高可用连接JedisSentinelPoll

Redis Sentinel是Redis官方提供的高可用解决方案,提供监控、通知和自动故障转移三个主要功能。

Redis Sentinel一般主要用于管理Redis服务器,主要是监控redis多台服务器,并在master宕机后,自动执行故障转移,从slave中选举出一台服务升级为新的master。

Redis Sentinel的详细介绍可以参考Redis 哨兵模式Sentinel

Redis Sentinel的部署可以参考Redis 哨兵模式部署章节

1.1. Jedis Sentinel 代码示例

Jedis客户端也提供了Redis Sentinel的支持,使用Jedis的Sentinel模式很简单,先看一下示例代码:

public class JedisSentinelsStartApp {

    public static final Logger LOGGER = LoggerFactory.getLogger(JedisSentinelsStartApp.class);

    public static void main(String[] args) {
        // redis 哨兵服务列表
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("127.0.0.1:26379");
        sentinels.add("127.0.0.1:26380");
        sentinels.add("127.0.0.1:26381");

        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(8);
        jedisPoolConfig.setMaxTotal(20);

        // 创建一个jedis哨兵连接池
        JedisSentinelPool sentinelPool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig);
        Jedis jedis = sentinelPool.getResource();
        jedis.set("sentinel-key1", "value1");
        jedis.set("sentinel-key2", "value2");

        Jedis jedis1 = new Jedis("localhost", 6379);
        String valueOfKey1 = jedis1.get("sentinel-key1");
        LOGGER.info("valueOfKey1:{}", valueOfKey1);

        String valueOfKey2 = jedis1.get("sentinel-key2");
        LOGGER.info("valueOfKey2:{}", valueOfKey2);

        jedis1.close();

        jedis.close();
        sentinelPool.close();
    }
}

使用Jedis Sentinel模式的主要步骤:

  1. 创建一个HashSet,存储redis sentinel服务器列表
  2. 创建JedisPoolConfig配置对象
  3. 创建JedisSentinelPool连接池对象,构造函数需要提供一个sentinel指定的mastername
  4. 从连接池中获取一个Jedis对象
  5. 使用Jedis对象操作Redis
  6. 把jedis连接对象归还到连接池
  7. 关闭连接池

看一下上面代码的日志:

十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Trying to find master from available Sentinels...
十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Redis master running at 127.0.0.1:6380, starting Sentinel listeners...
十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 127.0.0.1:6380
10:49:39.335 [main] INFO samples.cache.jedis.JedisSentinelsStartApp - valueOfKey1:value1
10:49:39.339 [main] INFO samples.cache.jedis.JedisSentinelsStartApp - valueOfKey2:value2

可以发现,JedisSentinelPool会获取当前的master地址,上述示例为127.0.0.1:6380

1.2. 原码分析

JedisSentinelPool的底层是基于Redis提供的下面功能实现:

  1. SENTINEL get-master-addr-by-name 命令来获取 master 地址;
  2. Pub/Sub 发布订阅功能,订阅sentinels发布的+switch-master频道,当redis主从切换时,redis会在此频道发送一条消息,jedis订阅此频道,获取到此消息后,进行本地master更新。
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;

    HostAndPort master = initSentinels(sentinels, masterName);
    initPool(master);
}

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");

    for (String sentinel : sentinels) {
      final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));

      log.fine("Connecting to Sentinel " + hap);

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort());

        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;

        if (masterAddr == null || masterAddr.size() != 2) {
          log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
              + ".");
          continue;
        }

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

    if (master == null) {
      if (sentinelAvailable) {
        // 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...");
      }
    }

    log.info("Redis master running at " + master + ", starting Sentinel listeners...");

    for (String sentinel : sentinels) {
      final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
      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;
}

可以看到,JedisSentinelPool的构造函数中,主要做了几件事:

  1. 循环所有的sentinels,使用Jedis连接sentinel服务器,调用jedis.sentinelGetMasterAddrByName(masterName)来获取master服务器地址
     public List<String> sentinelGetMasterAddrByName(String masterName) {
         client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);
         final List<Object> reply = client.getObjectMultiBulkReply();
         return BuilderFactory.STRING_LIST.build(reply);
     }
    
    底层使用的是SENTINELget-master-addr-by-name命令:
     $ bin/redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
     1) "127.0.0.1"
     2) "6380"
    
  2. 如果sentinel正确返回master地址,则执行下一步,否则使用下一个sentinel重新第1,第2步
  3. 如果循环完所有的sentinels都没有返回正确的master地址,根据sentinel是否可用,抛出异常,否则执行步骤4
  4. 如果master已获取到,则添加一个监听器MasterListener,此监听器主要用来订阅redis发布的master变更频道:+switch-master,核心代码:

     j.subscribe(new JedisPubSub() {
                     @Override
                     public void onMessage(String channel, String message) {
                       log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");
    
                       String[] switchMasterMsg = message.split(" ");
    
                       if (switchMasterMsg.length > 3) {
    
                         if (masterName.equals(switchMasterMsg[0])) {
                           initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                         } else {
                           log.fine("Ignoring message on +switch-master for master name "
                               + switchMasterMsg[0] + ", our master name is " + masterName);
                         }
    
                       } else {
                         log.severe("Invalid message received on Sentinel " + host + ":" + port
                             + " on channel +switch-master: " + message);
                       }
                     }
                   }, "+switch-master");
    

    redis 发布的 +switch-master 频道消息格式:

     +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
    

    所以上述代码中的initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));是以新master地址初始化连接池。

  5. 看一下JedisSentinel的getResource实现

     public Jedis getResource() {
     while (true) {
       Jedis jedis = super.getResource();
       jedis.setDataSource(this);
    
       // get a reference because it can change concurrently
       final HostAndPort master = currentHostMaster;
       final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
           .getPort());
    
       if (master.equals(connection)) {
         // connected to the correct master
         return jedis;
       } else {
         returnBrokenResource(jedis);
       }
     }
    }
    

    JedisSentinelPool每次从连接池获取链接对象的时候,都要对连接对象进行检测,如果此对象和sentinel的master服务地址不一致,则会关闭此连接,重新获取新的Jedis连接对象。

2. 参考

Copyright © wychuan.com 2017 all right reserved,powered by Gitbook该文件修订时间: 2017-10-17 02:48:52

results matching ""

    No results matching ""