Redis(九)高可用专栏之Sentinel模式

本文讲述Redis高可用方案中的哨兵模式——Sentinel,RedisClient中的Jedis如何使用以及使用原理。html

  • Redis主从复制
  • Redis Sentinel模式
  • Jedis中的Sentinel

Redis主从复制

Redis主从复制是Sentinel模式的基石,在学习Sentinel模式前,须要理解主从复制的过程。git

1.保证数据一致性的机制

Redis主从复制的含义和Mysql的主从复制同样,即利用Slave从服务器同步Master服务器数据的副本。主从复制的最为关键的点在于主从数据的一致性,在Redis中主要经过如下三点:github

  • 当Master和Slave链接正常时,Master会源源不断的发送命令流至Slave,更新Slave的数据,保证主从数据的一致性,其中包括:写入、过时和驱逐等操做;
  • 当Master和Slave之间出现链接断开或者链接超时等状况,当Slave从新链接上Master时,Slave会主动请求Master进行部分重同步——即在链接断开的窗口内,Master数据集变化的部分同步至Slave,保证其数据一致性;
  • 当没法进行部分重同步时,Slave则请求进行全量同步;

利用以上三点,Redis的主从复制保证数据的最终一致性。redis

2.主从复制的工做流程

假设有两台服务器,一台是Master,另外一台是Slave。如今需求是保证Master和Slave的数据一致性。
若是要保证精确的一致性,最好的方式是实时的进行全量同步,基于全量确定是一致的。可是这样形成的性能损耗必然不可估计。
增量同步即同步变化的数据,不一样步未发生变化的数据,虽然实现程度比全量复杂,可是能让性能提高。
Redis中实现主从复制是全量结合增量实现。sql

增量同步,必须获取主从服务器之间的数据差别,对于数据同一份数据的差别获取,最多见的方式即版本控制。如常见的版本控制系统:svn、git等。在Redis主从关系中,数据的最初来源于Master,因此数据版本控制由Master控制。数据库

Notes:
同一份数据的演变记录,最好的方式即版本控制安全

在Redis中,每一个Master都有一个RelicationID,标识一个给定的历史数据集,是一串伪随机串。同时还有一个OffsetID,当Master将变化的数据发送给Slave时,发送多少个字节,相应的offsetID就增加多少,依据此作数据集的版本控制。即便没有Slave,Master也会增加OffsetID,一个RelicationID和OffsetID的组合都会标识一个数据集版本。服务器

当Slave链接到Master时,Slave会向Master主动发送本身的RelicationID和OffsetID,Master依此判断Slave当前的数据版本,将变化的数据发送给Slave。当Slave发送的是一个未知的RelicationID和OffsetID,Master则会进行一次全同步。网络

Master会开启另外一个复制进程。复制进程会建立一个持久化的RDB快照文件,并将新的请求命令缓冲在缓冲区中,达到Copy-On-Write的效果。在RDB文件建立完成后,会将RDB文件发送给Slave,Slave接收到后,将文件保存至磁盘,而后再载入内存。最后Master再将缓冲区的命令流发送给Slave,完成最终的数据同步。多线程

对于主从复制还有不少特性,如:主从同步中的过时键处理主从之间的认证容许N个附加的副本Slave只读模式等,能够参考:复制

2.主从复制的配置

Redis主从复制的配置比较简单,分为两种方式:静态文件配置和动态命令行配置。redis.conf中提供:

slaveof 192.168.1.1 6379

配置项用于配置Slave节点的Master节点,表示是谁的Slave。

同时还能够在redis-cli命令行中使用slaveof 192.168.1.1 6379格式的命令配置一个Slave的Master节点。可使用slaveof no one取消其从节点的身份。

Redis Sentinel模式

1.为何须要Sentinel

Redis已经具有了主从复制的功能,为何仍然须要Sentinel模式?

Redis的主从模式从必定从程度上的确解决了可用性问题,这毋庸置疑。可是只仅仅主从复制来完成可用性,就比较简陋,灵活性不够,操做复杂。更不用说高可用!

  1. 当Master宕机,须要运维人员干预将Slave提高至新的Master,或者脚本自动化完成,可是都没法避免问题的复杂化;
  2. 客户端应用须要切换至新的Master,这点多是最大的痛点,应用没法自动切换至新的Master,没法完成自动的故障转移,不够灵活,没法高可用;

基于以上的需求,Redis Sentinel是Redis提供的高可用的一种模型,在Sentinel模式下,无需人员的干预,Sentinel可以帮助完成如下工做:

  • 监控:Sentinel可以持续不断的检查Master和Slaves是否在正常工做;
  • 通知:Sentinel能够以Api的方式通知另外一个程序或者管理员:发生错误的Redis实例;
  • 自动化故障转移:若是Master发生故障,Sentinel将开始故障转移,在这过程当中将提高一个Slave为新的Master,将其余的Slave从新配置其Master为新提高的Master,并通知使用Redis的应用程序使用新的Master;
  • 配置提供者:应用链接上Sentinel,能够获取整个高可用组中的Slave和Master的信息,Sentinel充当着客户端服务发现的来源;
2.Sentinel是什么

Sentinel自己就是一个分布式系统。Sentinel基于一个配置运行多个进程协同工做,这些进程能够在一个服务器实例上,也能够分布在多个不一样实例上。多个Sentinel工做有以下特色:

  • 当多个Sentinel认为一个Master不可用时,将会发起失败检测,下降误报的可能性。好比某些Sentinel由于与Master网络问题致使的误报;
  • 即便不是全部的Sentinel进程都是无缺的,Sentinel仍然可以正常的工做,解决了Sentinel自己的单点问题;

在Sentinel体系中,Sentinel、Redis实例和链接到Sentinel和Redis实例的应用这三者也共同组成了一个完整的分布式系统。

3.搭建Sentinel

Redis中提供了搭建Sentinel的相关命令:redis-sentinel。其中Redis包中也包含了sentinel.conf的示例配置。

启动Sentinel实例,能够直接运行:

redis-sentinel sentinel.conf

可是在配置sentinel模式前,现须要作些准备工做:

  1. 至少须要准备三台sentinel实例,解决sentinel自己的单点问题。若是是线上,最好保证sentinel的实例是不一样的机器;
  2. 须要使用支持Sentinel模式的client;
  3. 须要保证Sentinel实例之间的网络连通,Sentinel采用自动服务发现机制发现其余的Sentinel;
  4. 须要保证Sentinel和Redis实例之间的网络连通,Sentinel须要实时的获取Master和Slave信息并与其交互;

关于Sentinel系统的其余关注点,请参考:Fundamental things to know about Sentinel before deploying

下面看下Sentinel的配置文件:

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

sentinel monitor
用于配置sentinel的名称,master节点,仲裁数。

  • master-group-name:用于指定一个惟一的sentinel名称;
  • ip、port:master节点的ip和port;
  • quorum:认为Master不可用的sentinel进程数量时,尝试发起故障转移。可是并非当即进行,而只仅仅做为用于检测是否有故障。对于实际发起故障转移,sentinel须要进行选举,整个过程须要整个sentinel进程中的大多数投票表决;

举个例子,假设有5个sentinel进程:

  1. 其中有两个进程认为master不可用,则其一尝试进行故障转移;
  2. 若是至少有三个sentinel可用,则进行实际的故障转移;

down-after-milliseconds
用于配置sentinel认为Redis实例不可用的至少时间时多少,以毫秒为单位

sentinel parallel-syncs
用于配置故障转移后,同时进行从新配置slave节点的个数。从新配置slave时,则slave将没法处理客户端的查询请求。若是同时配置全部的slave,则将会出现,整个Redis不可用。可是若是该值较小,又会致使从新配置时间过长。须要trade off。

sentinel failover-timeout
用于配置故障转移的超时时间

接下来实际演示配置Redis Sentinel过程:

  • 准备环境
  • 编写配置:Sentinel conf和Redis conf
  • 启动Redis Sentinel

准备环境,因为笔者没有如此多的服务器,虽然可使用Docker,可是为了简单,直接使用一台机器,监听不一样端口实现。

#sentinel实例
127.0.0.1:26379
127.0.0.1:26380
127.0.0.1:26381
127.0.0.1:26382
127.0.0.1:26383

#redis实例
127.0.0.1:6379
127.0.0.1:6380
127.0.0.1:6381

编写sentinel的配置:

port 26379
dir "/Users/xxx/redis/sentinel/data"
logfile "/Users/xxx/redis/sentinel/log/sentinel_26379.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

其余的sentinel实例配置依次类推,分别使用26380,26381,26382,26383端口,日志文件名称也作相应更换。主机节点使用127.0.0.1:6379。

配置6379端口的Redis实例以下:

port 6379
daemonize yes
logfile "/Users/xxx/redis/sentinel/log/6379.log"
dbfilename "dump-6379.rdb"
dir "/Users/xxx/redis/sentinel/data"

6380和6381端口另外再加上一行配置:slaveof 127.0.0.1 6379,表示slave节点。

再分别启动Redis实例和Sentinel实例:

redis-server redis6379.conf
....
redis-sentinel sentinel26379.conf &

启动结束后能够查找Redis的相关进程有:

501  2165     1   0  7:47下午 ??         0:00.55 redis-server *:6379 
501  2167     1   0  7:47下午 ??         0:00.58 redis-server *:6380 
501  2171     1   0  7:47下午 ??         0:00.59 redis-server *:6381 
501  2129  1890   0  7:39下午 ttys000    0:02.03 redis-sentinel *:26379 [sentinel] 
501  2130  1890   0  7:39下午 ttys000    0:01.99 redis-sentinel *:26380 [sentinel] 
501  2131  1890   0  7:39下午 ttys000    0:02.02 redis-sentinel *:26381 [sentinel] 
501  2132  1890   0  7:39下午 ttys000    0:01.97 redis-sentinel *:26382 [sentinel] 
501  2133  1890   0  7:39下午 ttys000    0:01.93 redis-sentinel *:26383 [sentinel]

表示整个Redis Sentinel模式搭建完毕!

可使用redis-cli命令行链接到Sentinel查询相关信息

redis-cli -p 26379

#查询sentinel中的master节点信息和状态,考虑篇幅,这里只展现部分
127.0.0.1:26379> sentinel master mymaster
 1) "name"
 2) "mymaster"
 3) "ip"
 4) "127.0.0.1"
 5) "port"
 6) "6379"
 7) "runid"
 8) "67065dc606ffeb58d1b11e336bc210598743b676"
 9) "flags"
10) "master"
11) "link-pending-commands"

#查询sentinel中的slaves节点信息和状态,考虑篇幅,这里只展现部分
127.0.0.1:26379> sentinel slaves mymaster
1)  1) "name"
    2) "127.0.0.1:6381"
    3) "ip"
    4) "127.0.0.1"
    5) "port"
    6) "6381"
    7) "runid"
    8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08"
    9) "flags"
   10) "slave"

这里能够将master节点的进程kill,sentinel会自动进行故障转移。

kill -9 2165

#再查询master时,sentinel已经进行了故障转移
127.0.0.1:26379> sentinel master mymaster
 1) "name"
 2) "mymaster"
 3) "ip"
 4) "127.0.0.1"
 5) "port"
 6) "6381"
 7) "runid"
 8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08"
 9) "flags"
10) "master"

sentinel get-master-addr-by-name mymaster命令用于获取master节点

Notes:
以上的sentinel配置中并无配置slave相关的信息,只配置master节点。sentinel能够根据master节点获取全部的slave节点。

最后再来看下Sentinel中的Pub/Sub,Sentinel堆外提供了事件通知机制。Client能够订阅Sentinel的指定通道获取特定事件类型的通知。通道名称和事件名称相同,例如redis-cli - 23679登陆sentinel,订阅subcribe +sdown通道,而后kill监听6379的Redis实例,则会收到以下通知:

1) "pmessage"
2) "*"
3) "+sdown"
4) "slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381"

Redis Sentinel模式下的Client都是利用其特色,实现应用的故障自动转移。

关于Sentinel还有不少其余的功能特性,如:增长移除一个sentinel,增长移除slave等,更多细节,请参靠Redis Sentinel Documentation

Jedis中的Sentinel

前文中提到Redis Sentinel模式须要应用客户端的支持才能实现故障自动转移,切换至新提高的master节点上。同时也讲解Redis Sentinel系统提供了Pub/Sub的API供应用客户端订阅Sentinel的特定通道获取相应的事件类型的通知。

在Jedis中就是利用这些特色完成对Redis Sentinel模式的支持。下面按部就班的探索Jedis中的Sentinel源码实现。

Jedis中实现Sentinel只有一个核心类JedisSentinelPool,该类实现了:

  • 获取Sentinel中的master节点;
  • 实现自动故障转移;
1.JedisSentinelPool使用方式

JedisSentinelPool直接提供了构造函数API,能够直接利用sentinel的信息集合构造JedisSentinelPool,其中的getResource直接返回与当前master相关的Jedis对象。

@Test
public void sentinel() {
    Set<String> sentinels = new HashSet<>();
    sentinels.add(new HostAndPort("localhost", 26379).toString());
    sentinels.add(new HostAndPort("localhost", 26380).toString());
    sentinels.add(new HostAndPort("localhost", 26381).toString());
    sentinels.add(new HostAndPort("localhost", 26382).toString());
    sentinels.add(new HostAndPort("localhost", 26383).toString());

    String sentinelName = "mymaster";
    JedisSentinelPool pool = new JedisSentinelPool(sentinelName, sentinels);
    Jedis redisInstant = pool.getResource();
    System.out.println("current host:" + redisInstant.getClient().getHost() +
            ", current port:" + redisInstant.getClient().getPort());
    redisInstant.set("testK", "testV");

    // 故障转移
    Jedis sentinelInstant = new Jedis("localhost", 26379);
    sentinelInstant.sentinelFailover(sentinelName);

    System.out.println("current host:" + redisInstant.getClient().getHost() +
            ", current port:" + redisInstant.getClient().getPort());
    Assert.assertEquals(redisInstant.get("testK"), "testV");
}
2.JedisSentinelPool中成员域
public class JedisSentinelPool extends JedisPoolAbstract {

  // 链接池配置
  protected GenericObjectPoolConfig poolConfig;

  // 默认创建tcp链接的超时时间
  protected int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
  // socket读写超时时间
  protected int soTimeout = Protocol.DEFAULT_TIMEOUT;

  // 认证密码
  protected String password;

  // Redis中的数据库
  protected int database = Protocol.DEFAULT_DATABASE;

  protected String clientName;

  // 故障转移器,用于实现master节点切换
  protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();

  protected Logger log = LoggerFactory.getLogger(getClass().getName());

  // 建立与Redis实例的链接的工厂,使用volatile,保证多线程下的可见性
  private volatile JedisFactory factory;
 
  // 当前正在使用的master节点,使用volatile,保证多线程下的可见性
  private volatile HostAndPort currentHostMaster;
}
3.JedisSentinelPool的构造过程

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
  HostAndPort master = initSentinels(sentinels, masterName);
  // 初始化redis实例链接池
  initPool(master);
}

继续看initSentinels过程

// sentinels是sentinel配置:ip/port
// masterName是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...");

  // 循环处理每一个sentinel,寻找master节点
  for (String sentinel : sentinels) {
    // 解析字符串ip:port -> HostAndPort对象
    final HostAndPort hap = HostAndPort.parseString(sentinel);

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

    Jedis jedis = null;
    try {
      // 建立与sentinel对应的jedis对象
      jedis = new Jedis(hap);
      // 从sentinel获取master节点
      List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

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

      // 若是为空,或者不是ip和port组成的size为2的list,则处理下一个sentinel
      if (masterAddr == null || masterAddr.size() != 2) {
        log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap);
        continue;
      }

      // 构形成表示master的HostAndPort对象
      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();
      }
    }
  }

  // 若是master为空,则sentinel异常,throws ex
  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...");

  // 遍历sentinel集合,对每一个sentinel建立相应的监视器
  // sentinel自己是集群高可用,这里须要为每一个sentinel建立监视器,监视相应的sentinel
  // 即便sentinel挂掉一部分,仍然可用
  for (String sentinel : sentinels) {
    final HostAndPort hap = HostAndPort.parseString(sentinel);
    // 建立sentinel监视器
    MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
    // whether MasterListener threads are alive or not, process can be stopped
    // sentinel设置为守护线程
    masterListener.setDaemon(true);
    masterListeners.add(masterListener);
    // 启动线程监听sentinel的事件通知
    masterListener.start();
  }

  return master;
}

初始化sentinel中的主要逻辑分为两部分:

  • 经过遍历sentinel,寻找redis的master节点,只要寻找到遍历遍结束;
  • 遍历sentinel,为每一个sentinel建立线程监听器;

下面继续探索initPool方法,该方法以初始化setntinel中寻找的master节点为参数,进行初始化jedis与redis的master节点的JedisFactory。

// 该过程主要是为了初始化jedis与master节点的JedisFactory对象
// 一旦JedisFactory被初始化,应用就能够用其建立操做master节点相关的Jedis对象
private void initPool(HostAndPort master) {
  // 判断当前的master节点是否与要设置的master相同,currentHostMaster是volatile变量
  // 保证线程可见性
  if (!master.equals(currentHostMaster)) {
    // 若是不相等,则从新设置当前的master节点
    currentHostMaster = master;
    // 若是factory是空,则利用新的master建立factory
    if (factory == null) {
      factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
          soTimeout, password, database, clientName);
      initPool(poolConfig, factory);
    } else {
      // 不然更新factory中的master节点
      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);
  }
}

initPool中完成了应用于redis的master节点的链接建立,Jedis对象工厂的建立。
这样应用就可使用JedisSentinelPool的getResource方法获取与master节点对应的Jedis对象对master节点进行读写。这些步骤主要用于应用启动时执行与master节点的初始化操做。可是在应用运行期间,若是sentinel的master发生故障转移,应用如何实现自动切换至新的master节点,这样的功能主要是sentinel监视器MasterListener完成。接下来主要分析MasterListener的实现。

// MasterListener自己是一个线程对象的实现,因此sentinel模式中有几个sentinel进程
// 应用就会为其建立多少个相对应的线程监听,这样主要是为了保证sentinel自己的高可用
protected class MasterListener extends Thread {
    // sentinel的名称,应用一样的Redis实例群体能够组建不一样的sentinel
    protected String masterName;
    // 对应的sentinel host
    protected String host;
    // 对应的端口
    protected int port;
    // 订阅重试的等待时间,前文中介绍,实现自动故障转移的核心是利用sentinel提供的
    // pub/sub API,实现订阅相应类型通道,接受相应的事件通知
    protected long subscribeRetryWaitTimeMillis = 5000;
    // 与sentinel链接操做的Jedis
    protected volatile Jedis j;
    // 表示对应的sentinel是否正在运行
    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;
    }
}

实现自动转移至新提高的master节点的逻辑在run方法中

@Override
public void run() {
  // 线程第一次启动时,设置sentinel运行标识为true
  running.set(true);

  // 若是该sentinel仍然活跃,则循环
  while (running.get()) {

    // 建立与该sentinel对应的jedis对象,用于操做该sentinel
    j = new Jedis(host, port);

    try {
      // 再次检查,由于在以上的操做期间,该sentinel可能会销毁,能够查看shutdown方法
      // 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{
          // 若是master合法,则调用initPoolf方法初始化与master节点的JedisFactory
          initPool(toHostAndPort(masterAddr)); 
      }

      // 订阅该sentinel的+switch-master通道。+switch-master通道的事件类型为故障转移,切换新的master的事件类型
      j.subscribe(new JedisPubSub() {
        // redis sentinel中一旦发生故障转移,切换master。就会收到消息,消息内容为新提高的master节点
        @Override
        public void onMessage(String channel, String message) {
          log.debug("Sentinel {}:{} published: {}.", host, port, message);

          // 解析消息获取新提高的master节点
          String[] switchMasterMsg = message.split(" ");

          if (switchMasterMsg.length > 3) {

            if (masterName.equals(switchMasterMsg[0])) {
              // 将应用的当前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");

    } catch (JedisException e) {
      // 若是繁盛异常,判断对应的sentinel是否仍然处于运行状态
      if (running.get()) {
        // 若是是处于运行,则是链接问题,线程睡眠subscribeRetryWaitTimeMillis毫秒,而后while循环继续订阅+switch-master通道
        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();
    }
  }
}

以上的应用端实现故障发生时自动切换master节点的逻辑,注释已经讲述的很是清晰。这里须要关注的几点问题:

  1. 由于sentinel进程可能有多个,保证自身高可用。因此这里MasterListener对应也有多个,因此对于实现切换master节点是多线程环境。其中优秀的地方在于没有使用任何的同步,只是利用volatile保证可见性。由于对currentMaster和factory变量的操做,都只是赋值操做;

  2. 由于是多线程,因此initPool会被调用屡次。一个是应用启动的main线程,还有就是N个sentinel对应的MasterListener监听线程。因此initPool被调用N+1次,同时发生故障转移时,将会被调用N次。可是即便是屡次初始化,master的参数都是同样,基本上不会出现线程安全问题;

到这里,Redis的Sentinel模式和Jedis中实现应用端的故障自动转移就探索结束。下面再总结下Redis Sentinel模式在保证高可用的前提下的缺陷。

总结

Redis Setninel模式当然结局了Redis单机的单点问题,实现高可用。可是它是基于主从模式,不管任何主从的实现,其中最为关键的点就是数据一致性。在软件架构中二者数据一致性的实现方式可谓五花八门:

  1. 二者之间进行异步复制数据,保证数据一致性(可软件自实现或者第三方组件进行异步复制);
  2. 同步回写方式。应用写主时,主在back写入从,主再返回应用响应;
  3. 双写方式:应用既写主又写从(可是从通常都设置只读模式);

在主从模式中,实现一致性,大多数是利用异步复制的方式,如:binlog、dumpfile、commandStream等等,且又分为全量和增量方式结合使用。

通过以上描述,提出的问题:

  1. 由于是异步复制,必然就存在必定的时间窗口期间,主从的数据是不一致的,那么就有可能出现,数据不一致的场景(即便很难发生);
  2. 有数据不一致场景,就有可能出现数据丢失问题(如主宕机,从切换为主,可是主的一部分数据未能异步复制,致使从的数据丢失一部分);
  3. sentinel虽然实心了故障转移,可是故障转移也是有必定的时间的,这段时间无主可用;

在使用主从模式中,不少状况下为保证性能,常将master的持久化关闭,因此常常会出现主从所有宕机,当主从自启动后,出现master的键空间为空,从又异步同步主,致使从同步空的过来,致使主从数据都出现丢失!

在Redis Sentinel模式中尽可能设置主从禁止自启动,或者主开启持久化功能。

参考

Redis Sentinel Documentation
jedis