Redis之Sentinel应用及源码分析

Jedis 有4 种工作模式:单节点、分片(Sharded)、哨兵(Sentinel)、集群(Cluster)。上一篇我们通过实际应用的例子进行了源码剖析,本文将通过实际的例子对Redis的Sentinel进行源码剖析。

分片(Sharded)、集群(Cluster)请参考:

Redis之Sharded应用及源码分析

Redis之Cluster应用及源码分析

1、环境:

对于Redis Sentinel的搭建请参考一篇学会Redis Sentinel的集群搭建

1、操作系统:Centos7.7

2、服务器配置如下:(本文采用了在单台机子上部署多个实例来模拟多机部署)

主机ip角色和端口
master132.232.125.1966380
slave1132.232.125.1966381
slave2132.232.125.1966382
Sentinel132.232.125.19616380
Sentinel132.232.125.19616381
Sentinel132.232.125.19616382

2、启动redis和Sentinel服务

[root@m logs]# ps -ef|grep redis
root     13037     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-server *:6380
root     13042     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-server *:6381
root     13049     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-server *:6382
root     13942     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-sentinel *:16380 [`sentinel`]
root     13947     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-sentinel *:16381 [`sentinel`]
root     13952     1  0 18:07 ?        00:00:00 /usr/local/soft/redis-5.0.8/src/redis-sentinel *:16382 [`sentinel`]
root     14019   937  0 18:07 ?        00:00:00 [redis-server] <defunct>
root     14021 21486  0 18:07 pts/1    00:00:00 grep --color=auto redis

3、测试代码

package nci.henry;

import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Set;

/**
 * @Author: henry
 * @Date: 2020/4/18 15:43
 * @Description: 测试Sentinel
 */
public class JedisSentinelTest {


    private JedisSentinelPool pool;

    @Before
    public void initJedis(){
        // master的名字是sentinel.conf配置文件里面的名称
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("132.232.115.96:16380");
        sentinels.add("132.232.115.96:16381");
        sentinels.add("132.232.115.96:16382");
        pool = new JedisSentinelPool(masterName, sentinels);
    }

    @Test
    public void testGet(){
        try {
            pool.getResource().set("henry", "time:" + new Date());
            System.out.println(pool.getResource().get("henry"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行 testGet()方法,结果如下:

time:Sat Apr 18 18:32:27 CST 2020

疑问

Jedis 连接Sentinel 的时候,我们配置的是全部哨兵的地址。Sentinel 是如何返回可用的master 地址的呢?下面我们通过分析源码,看一下Sentinel 具体实现方式。

4、源码分析

4.1 原理

1)、客户端连接到哨兵集群后,通过发送Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME命令;

2)、从哨兵机器中询问master节点的信息,拿到master节点的ip端口号以后,再到客户端发起连接。

3)、建立连接以后,需要在客户端建立监听机制,当master重新选举之后,客户端需要重新连接到新的master节点。

4.2 JedisSentinelPool构造方法

pool = new JedisSentinelPool(masterName, sentinels);

先来看下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,
      final int sentinelConnectionTimeout, final int sentinelSoTimeout, final String sentinelPassword,
      final String sentinelClientName) {

    this.poolConfig = poolConfig;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;
    this.sentinelConnectionTimeout = sentinelConnectionTimeout;
    this.sentinelSoTimeout = sentinelSoTimeout;
    this.sentinelPassword = sentinelPassword;
    this.sentinelClientName = sentinelClientName;

    HostAndPort master = initSentinels(sentinels, masterName);
    //首次调用initPool()方法
    initPool(master);
  }

在构造方法中调用了initSentinels()方法,此方法是用来初始化Sentinel集群;

4.3 Sentinel集群初始化

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

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");
    // 有多个sentinels,遍历这些个sentinels
    for (String sentinel : sentinels) {

      // host:port 表示的sentinel 地址转化为一个HostAndPort 对象。
      final HostAndPort hap = HostAndPort.parseString(sentinel);

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

      Jedis jedis = null;
      try {
        // 连接到sentinel
        jedis = new Jedis(hap.getHost(), hap.getPort(), sentinelConnectionTimeout, sentinelSoTimeout);
        if (sentinelPassword != null) {
          jedis.auth(sentinelPassword);
        }
        if (sentinelClientName != null) {
          jedis.clientSetname(sentinelClientName);
        }
        // 根据masterName 得到master 的地址,返回一个list,host= list[0], port =// list[1]
        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;
        }
        // 如果在任何一个sentinel 中找到了master,不再遍历sentinels
        master = toHostAndPort(masterAddr);
        log.debug("Found Redis master at {}", 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();
        }
      }
    }
    // 到这里,如果master 为null,则说明有两种情况,一种是所有的sentinels节点都down掉了,一种是master
    //节点没有被存活的sentinels 监控到
    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...");
      }
    }
    // 如果走到这里,说明找到了master 的地址
    log.info("Redis master running at " + master + ", starting Sentinel listeners...");

    // 启动对每个sentinels 的监听为每个sentinel 都启动了一个监听者MasterListener。MasterListener 本身是一个线
//    程,它会去订阅sentinel 上关于master 节点地址改变的消息。
    for (String sentinel : sentinels) {
      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;
  }

根据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);
  }

通过以上源码可以得出如下初始化流程:

  1. 遍历Sentinel节点集合,找到一个可用的Sentinel节点,如果找不到就从Sentinel节点集合中去找下一个;如果都找不到直接抛出异常给客户端:
    2)找到一个可用的Sentinel节点, 执行sentinelGetMasterAddrByName( masterName),通过主机名称找到对应主节点信息:
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

3)JedisSentinelPool中没有发现对主节点角色验证的代码,这是因为get-master-addr-by-name master-name这个API本身就会自动获取真正的主节点(例如故障转移期间)。

 client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);

  public static final String SENTINEL_GET_MASTER_ADDR_BY_NAME = "get-master-addr-by-name";

4)得到 master 信息后,再次遍历哨兵集合,为每一个Sentinel节点单独启动一个线程,也可以称之为监听者MasterListener,监听哨兵的发布订阅消息,消息主题是 +switch-master. 当主节点发生变化时,将通过 pub/sub 通知该线程,该线程将更新 Redis 连接池。

4.4 MasterListener

来看一下MasterListener线程中的run方法:

@Override
    public void run() {

      running.set(true);

      // 死循环
      while (running.get()) {
        //创建一个 Jedis对象
        j = new Jedis(host, port);

        try {
          // 继续检查
          // double check that it is not being shutdown
          if (!running.get()) {
            break;
          }
          // code for active refresh
          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));
          }

          // jedis 对象,通过 Redis pub/sub 订阅 switch-master 主题
          //订阅sentinel上关于master地址改变的消息
          j.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
              log.debug("Sentinel {}:{} published: {}.", host, port, message);
              // 分割字符串
              String[] switchMasterMsg = message.split(" ");
              // 如果长度大于3
              if (switchMasterMsg.length > 3) {

                // 且第一个字符串的名称和当前 masterName 发生了 switch
                if (masterName.equals(switchMasterMsg[0])) {
                  // 重新初始化连接池(第 4 个和 第 5 个)
                  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");

        } catch (JedisException e) {
          // 如果连接异常
          if (running.get()) {
            log.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying.", host,
              port, e);
            try {
              // 默认休息 5 秒
              Thread.sleep(subscribeRetryWaitTimeMillis);
            } catch (InterruptedException e1) {
              log.error("Sleep interrupted: ", e1);
            }
          } else {
            log.debug("Unsubscribing from Sentinel at {}:{}", host, port);
          }
        } finally {
          j.close();
        }
      }
    }

1、对每一个哨兵节点通过一个 MasterListener 进行监听(Redis的发布订阅功能),订阅哨兵节点+switch-master频道;

2、当发生故障转移时,即master地址变换时,就会再调用一次initPool()方法客户端能收到哨兵的通知,通过重新初始化连接池,完成主节点的切换。

4.4 initPool()方法

 private void initPool(HostAndPort master) {
    synchronized(initPoolLock){
        //master与currentHostMaster比较,master没有改变则不需要initPool
        //  private volatile HostAndPort currentHostMaster;
      if (!master.equals(currentHostMaster)) {//
        currentHostMaster = master;
          //首次调用,实例化Jedis工厂
        if (factory == null) {
          factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
              soTimeout, password, database, clientName);
          initPool(poolConfig, factory);
        } else {
           //非首次调用,修改工厂设置
          factory.setHostAndPort(currentHostMaster);
          // although we clear the pool, we still have to check the
          // returned object
          // in getResource, this call only clears idle instances, not
          // borrowed instances
          internalPool.clear();
        }

        log.info("Created JedisPool to master at " + master);
      }
    }
  }

  • 1)、master与实例变量currentHostMaster作比较,只有当master值改变后,才进入方法调用initPool()方法;

  • 2)、如果是第一次调用initPool方法(构造函数中调用),那么会初始化Jedis实例创建工厂,如果不是第一次调用(MasterListener中调用),那么只对已经初始化的工厂进行重新设置。

  • 3)、从以上也可以看出为什么currentHostMasterfactory这两个变量为什么要声明为volatile,它们会在多线程环境下被访问和修改,因此必须保证可见性

首次调用initPool()方法,是在 JedisSentinelPool构造方法中,进入判断逻辑里开始调用initPool(poolConfig, factory);,此处调用的是Pool.java里的方法,用来初始化内部对象池。

//Pool.java
public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {

    if (this.internalPool != null) {
      try {
        closeInternalPool();
      } catch (Exception e) {
      }
    }

    this.internalPool = new GenericObjectPool<T>(factory, poolCofig);
  }

结语:

通过源码的分析,能使我们对Redis的Sentinel原理更加清楚,后续将继续对Cluster的原理进行源码分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值