1. 程式人生 > >Redis(九)高可用專欄之Sentinel模式

Redis(九)高可用專欄之Sentinel模式

置疑 登錄 ann 過期鍵 proto handle cli 也有 取整

本文講述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

Redis(九)高可用專欄之Sentinel模式