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

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

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

Redis主从复制

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

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

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

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

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

2.主从复制的工作流程

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

增量同步,必须获取主从服务器之间的数据差异,对于数据同一份数据的差异获取,最常见的方式即版本控制。如常见的版本控制系统: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

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

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

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

down-after-milliseconds

sentinel parallel-syncs

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

猜你喜欢

转载自www.cnblogs.com/lxyit/p/9829120.html