Redis(十一):哨兵模式架構設計分析


  業務最初的應用場景中,我們也許使用單機redis就可以應付業務要求,但並非一直可行。

  比如單機的讀寫能力問題,單機的可用性問題,單機的數據安全性問題。這些都是許多互聯網應用經常會遇到的問題,也基本上都有一套理論去解決它,只是百花齊放。

  哨兵是Redis中解決高可用問題的解決方案之一,我們就一起來看看 Redis是如何實現的吧!不過此方案,僅提供思路供參考,不要以此為標准方案。

  前面介紹的主從復制功能,可以說已經一定程度上解決了數據安全性問題問題,即有了備份數據,我們可以可以做讀寫分離了。只是,可用性問題還未解決,即當 master 宕機或出現其他故障時,整個寫服務就不可用了。解決方法是,手動操作,要么重啟master使其恢復服務,要么把master切換為其他slave機器。

  如果服務的可用性需要人工介入的話,那就算不得高可用了,所以我們需要一個自動處理機制。這就是哨兵模式。

一、哨兵系統介紹

  哨兵系統要解決的問題核心,自然是高可用問題。而如何解決,則是其設計問題。而最終呈現給用戶的,應該一個個的功能單元,即其提供的能力。如下:

    監控(Monitoring): Sentinel 會不斷地檢查你的主服務器和從服務器是否運作正常。
    提醒(Notification): 當被監控的某個 Redis 服務器出現問題時, Sentinel 可以通過 API 向管理員或者其他應用程序發送通知。
    自動故障遷移(Automatic failover): 當一個主服務器不能正常工作時, Sentinel 會開始一次自動故障遷移操作, 它會將失效主服務器的其中一個從服務器升級為新的主服務器, 並讓失效主服務器的其他從服務器改為復制新的主服務器;
    配置提供者: Sentinel充當客戶端服務發現的授權來源:客戶端連接到Sentinels,以詢問負責給定服務的當前Redis主服務器的地址。 如果發生故障轉移,Sentinels將報告新地址。(這也是客戶端接入入口)

  哨兵系統的架構圖如下:

  (一)服務端架構

 

 

  (二)請求處理流程圖

 

 

二、哨兵系統搭建步驟

  哨兵可以搭建在 redis服務所在機器,也可以在單獨的機器實例上搭建。

  1. 有多個在運行的 redis master/slave 實例;

    主從服務的搭建,slaveof 設置,請參照主從配置篇。

  2. 編寫哨兵配置文件;

# Example sentinel.conf
# 定義sentinel 服務端口號
port 26379

# 針對 使用端口映射的方式的啟動,指定ip:port
# sentinel announce-ip <ip>
# sentinel announce-port <port>
# 工作目錄定義
dir /tmp

# 要監視的redis master 定義, 可配置多個 master-name 不同即可
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 定義master/slave 的密碼,要求同一主從服務所有密碼必須保持一致
# sentinel auth-pass <master-name> <password>

# 定義master 不可達持續多少毫秒后開始定義為節點下線,默認30s
sentinel down-after-milliseconds mymaster 30000

# sentinel parallel-syncs <master-name> <numslaves>
# 在故障轉移期間同時與新的master同步的slave數量
sentinel parallel-syncs mymaster 1

# 定義進行故障轉移的超時時間,默認3分鍾
sentinel failover-timeout mymaster 180000

# 發生故障轉移時調用的通知腳本,被調用時會傳遞兩個參數: eventType, eventDescription
# sentinel notification-script mymaster /var/redis/notify.sh

# master 變更時調用腳本配置
# 調用時會傳遞如下參數
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

  3. 啟動哨兵節點;

    # 使用 redis-sentinel 程序啟動, 這個程序不一定會有,需要自己編譯
    redis-sentinel /path/to/sentinel.conf
    # 使用 redis-server 程序啟動, 一定可用
    # 測試時可以加上   --protected-mode no, 在不設置密碼情況下訪問redis
    redis-server /path/to/sentinel.conf --sentinel

  4. 驗證哨兵運行情況

        通過redis-cli 連接到sentinel 服務內部:
        redis-cli -p 26379   # 連接到sentinel
        info sentinel         # 查看哨兵信息
        SENTINEL slaves mymaster    # 查看master下的slave服務器情況
        SENTINEL sentinels mymaster    # 查看master的哨兵服務器列表
        SENTINEL get-master-addr-by-name mymaster    # 獲取master地址信息

  5. 故障模擬

  將master節點關閉后,等待一段時間,再獲取master地址看看。master已經切換了。

   SENTINEL get-master-addr-by-name mymaster    # 獲取master地址信息

 

三、哨兵實現高可用的運行原理

1. Sentinel 的定時任務

  • 每個 Sentinel 以每秒鍾一次的頻率向它所知的主服務器、從服務器以及其他 Sentinel 實例發送一個 PING 命令。
  • 如果一個實例(instance)距離最后一次有效回復 PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 那么這個實例會被 Sentinel 標記為主觀下線。 一個有效回復可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
  • 如果一個主服務器被標記為主觀下線, 那么正在監視這個主服務器的所有 Sentinel 要以每秒一次的頻率確認主服務器的確進入了主觀下線狀態。
  • 如果一個主服務器被標記為主觀下線, 並且有足夠數量的 Sentinel (至少要達到配置文件指定的數量)在指定的時間范圍內同意這一判斷, 那么這個主服務器被標記為客觀下線。
  • 在一般情況下, 每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主服務器和從服務器發送 INFO 命令。 當一個主服務器被 Sentinel 標記為客觀下線時, Sentinel 向下線主服務器的所有從服務器發送 INFO 命令的頻率會從 10 秒一次改為每秒一次。
  • 當沒有足夠數量的 Sentinel 同意主服務器已經下線, 主服務器的客觀下線狀態就會被移除。 當主服務器重新向 Sentinel 的 PING 命令返回有效回復時, 主服務器的主觀下線狀態就會被移除。

2. 自動發現 Sentinel 和從服務器

  一個 Sentinel 可以與其他多個 Sentinel 進行連接, 各個 Sentinel 之間可以互相檢查對方的可用性, 並進行信息交換。

  Sentinel 可以通過發布與訂閱功能來自動發現正在監視相同主服務器的其他 Sentinel , 這一功能是通過向pub/sub頻道 sentinel:hello 發送信息來實現的。

  Sentinel 可以通過詢問主服務器來獲得所有從服務器的信息。

  • 每個 Sentinel 會以每兩秒一次的頻率, 通過發布與訂閱功能, 向被它監視的所有主服務器和從服務器的 sentinel:hello 頻道發送一條信息, 信息中包含了 Sentinel 的 IP 地址、端口號和運行 ID (runid)。
  • 每個 Sentinel 都訂閱了被它監視的所有主服務器和從服務器的 sentinel:hello 頻道, 查找之前未出現過的 sentinel (looking for unknown sentinels)。 當一個 Sentinel 發現一個新的 Sentinel 時, 它會將新的 Sentinel 添加到一個列表中, 這個列表保存了 Sentinel 已知的, 監視同一個主服務器的所有其他 Sentinel 。
  • Sentinel 發送的信息中還包括完整的主服務器當前配置(configuration)。 如果一個 Sentinel 包含的主服務器配置比另一個 Sentinel 發送的配置要舊, 那么這個 Sentinel 會立即升級到新配置上。
  • 在將一個新 Sentinel 添加到監視主服務器的列表上面之前, Sentinel 會先檢查列表中是否已經包含了和要添加的 Sentinel 擁有相同運行 ID 或者相同地址(包括 IP 地址和端口號)的 Sentinel , 如果是的話, Sentinel 會先移除列表中已有的那些擁有相同運行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。

3. 故障轉移

  一次故障轉移操作由以下步驟組成:

  • 發現主服務器已經進入客觀下線狀態。
  • 對我們的當前紀元進行自增(詳情請參考 Raft leader election ), 並嘗試在這個紀元中當選。
  • 如果當選失敗, 那么在設定的故障遷移超時時間的兩倍之后, 重新嘗試當選。 如果當選成功, 那么執行以下步驟。
  • 選出一個從服務器,並將它升級為主服務器。
  • 向被選中的從服務器發送 SLAVEOF NO ONE 命令,讓它轉變為主服務器。
  • 通過發布與訂閱功能, 將更新后的配置傳播給所有其他 Sentinel , 其他 Sentinel 對它們自己的配置進行更新。
  • 向已下線主服務器的從服務器發送 SLAVEOF 命令, 讓它們去復制新的主服務器。
  • 當所有從服務器都已經開始復制新的主服務器時, 領頭 Sentinel 終止這次故障遷移操作。

  每當一個 Redis 實例被重新配置(reconfigured) —— 無論是被設置成主服務器、從服務器、又或者被設置成其他主服務器的從服務器 —— Sentinel 都會向被重新配置的實例發送一個 CONFIG REWRITE 命令, 從而確保這些配置會持久化在硬盤里。

  Sentinel 使用以下規則來選擇新的主服務器:

  • 在失效主服務器屬下的從服務器當中, 那些被標記為主觀下線、已斷線、或者最后一次回復 PING 命令的時間大於五秒鍾的從服務器都會被淘汰。
  • 在失效主服務器屬下的從服務器當中, 那些與失效主服務器連接斷開的時長超過 down-after 選項指定的時長十倍的從服務器都會被淘汰。
  • 在經歷了以上兩輪淘汰之后剩下來的從服務器中, 我們選出復制偏移量(replication offset)最大的那個從服務器作為新的主服務器; 如果復制偏移量不可用, 或者從服務器的復制偏移量相同, 那么帶有最小運行 ID 的那個從服務器成為新的主服務器。

 

四、客戶端使用哨兵系統

  哨兵系統搭建好之后,就可以提供服務了。那么,如何提供服務呢?從最前面的兩張架構圖中,我們可以看到,sentinel 差不多是作為一個配置中心或者存在的,它只會為客戶端提供master/slave的相關信息,而並不會直接代替redis實例進行存取操作。所以,哨兵模式,需要客戶端做更多的工作,原來的直接連接redis變為間接從sentinel獲取信息,再連接,還要維護可能的信息變更。

  當然,這種工作一般是要交給sdk做的,實現原理也差不多,我們就以 jedis 作為切入點,詳解下客戶端如何使用sentinel.

  1. 引入pom依賴

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

  2. 單元測試

public class RedisSentinelTest {

    @Test
    public void testSentinel() throws Exception {
        // 池化基礎信息配置
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10);
        jedisPoolConfig.setMaxIdle(5);
        jedisPoolConfig.setMinIdle(5);
        // 哨兵連接信息配置
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<>();
        sentinels.add("127.0.0.1:26379");
        sentinels.add("127.0.0.1:26378");
        sentinels.add("127.0.0.1:26377");
        // 在redis需要使用密碼訪問時,傳入即可
        String password = null;
        // 使用 JedisSentinelPool 封裝哨兵的訪問細節
        JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels, jedisPoolConfig, password);
        Jedis jedis = pool.getResource();
        String key = "key1";
        String value = "Value1";
        jedis.set(key, value);
        System.out.println("set a value to Redis over. " + key + "->" + value);
        value = jedis.get("key1");
        System.out.println("get a value from Redis over. " + key + "->" + value);
        pool.close();
    }
}

 

  3. sentinel 處理過程解析

  jedis的sdk中已經將哨兵封裝得和普通的redis實例請求差不多了,所以,我們需要深入理解下其處理過程。

  首先是在初始化 JedisSentinelPool 時,其會與sentinel列表中選擇一個與其建立連接:

    // redis.clients.jedis.JedisSentinelPool#JedisSentinelPool
  public JedisSentinelPool(String masterName, Set<String> sentinels) {
    this(masterName, sentinels, new GenericObjectPoolConfig(), Protocol.DEFAULT_TIMEOUT, null,
        Protocol.DEFAULT_DATABASE);
  }
  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, int timeout, final String password,
      final int database) {
    this(masterName, sentinels, poolConfig, timeout, timeout, password, database);
  }
  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, final int timeout, final int soTimeout,
      final String password, final int database) {
    this(masterName, sentinels, poolConfig, timeout, soTimeout, password, database, null);
  }
  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中獲取master信息,關鍵
    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...");
    // 依次遍歷 sentinels, 直到找到一個可用的sentinel
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);

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

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort());
        // 向sentinel發送命令請求: SENTINEL get-master-addr-by-name mymaster, 獲取master地址信息
        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...");
    // 為每個 sentinel, 建立一個監聽線程, 監聽 sentinel 的 +switch-master 信息
    // 當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;
  }
    // 每個 sentinel 監聽線程事務處理流程如下
    // redis.clients.jedis.JedisSentinelPool.MasterListener#run
    @Override
    public void run() {

      running.set(true);

      while (running.get()) {

        j = new Jedis(host, port);

        try {
          // double check that it is not being shutdown
          if (!running.get()) {
            break;
          }
            // SUBSCRIBE +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(" ");
                // 格式為: masterName xx xx masterHost masterPort
              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");

        } catch (JedisConnectionException e) {

          if (running.get()) {
            log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port
                + ". Sleeping 5000ms and retrying.", e);
            try {
              Thread.sleep(subscribeRetryWaitTimeMillis);
            } catch (InterruptedException e1) {
              log.log(Level.SEVERE, "Sleep interrupted: ", e1);
            }
          } else {
            log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
          }
        } finally {
          j.close();
        }
      }
    }

  從上面流程我們也就可以看出客戶端是如何處理 sentinel 和 redis 的關系的了。簡單來說就是通過 sentinel get-master-addr-by-name xxx, 獲取master地址信息,然后連接過去就可以了。在master發生變化時,通過pub/sub訂閱sentinel信息,從而進行連接池的重置。

  這個連接池又是如何處理的呢?我們可以簡單看一下:

    // redis.clients.jedis.JedisSentinelPool#initPool
  private void initPool(HostAndPort master) {
    if (!master.equals(currentHostMaster)) {
      currentHostMaster = master;
      if (factory == null) {
        factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
            soTimeout, password, database, clientName, false, null, null, null);
        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);
    }
  }
  // redis.clients.util.Pool#initPool
  public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {

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

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

  當要向redis寫入數據時,會先從連接池里獲取一個連接實例,其池化框架使用的是 GenericObjectPool 的通用能力,調用 JedisFactory 的 makeObject() 方法進行創建 :

  // redis.clients.jedis.JedisSentinelPool#getResource
  @Override
  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());
      // host:port 比對,如果master未變化,說明獲取到了正確的連接,返回
      if (master.equals(connection)) {
        // connected to the correct master
        return jedis;
      } 
      // 如果master 發生了切換,則將當前連接釋放,繼續嘗試獲取master連接
      else {
        returnBrokenResource(jedis);
      }
    }
  }
  // redis.clients.util.Pool#getResource
  public T getResource() {
    try {
      return internalPool.borrowObject();
    } catch (NoSuchElementException nse) {
      throw new JedisException("Could not get a resource from the pool", nse);
    } catch (Exception e) {
      throw new JedisConnectionException("Could not get a resource from the pool", e);
    }
  }
    // org.apache.commons.pool2.impl.GenericObjectPool#borrowObject()
    @Override
    public T borrowObject() throws Exception {
        return borrowObject(getMaxWaitMillis());
    }
    // org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)
    public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
        assertOpen();

        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
                (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3) ) {
            removeAbandoned(ac);
        }

        PooledObject<T> p = null;

        // Get local copy of current config so it is consistent for entire
        // method execution
        final boolean blockWhenExhausted = getBlockWhenExhausted();

        boolean create;
        final long waitTime = System.currentTimeMillis();

        while (p == null) {
            create = false;
            p = idleObjects.pollFirst();
            if (p == null) {
                // 沒有獲取到連接時,主動創建一個
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        p = idleObjects.takeFirst();
                    } else {
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
            } else {
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
            }
            if (!p.allocate()) {
                p = null;
            }

            if (p != null) {
                try {
                    // 確保激活當前數據庫
                    factory.activateObject(p);
                } catch (final Exception e) {
                    try {
                        destroy(p);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        validate = factory.validateObject(p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    if (!validate) {
                        try {
                            destroy(p);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                    "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }

        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

        return p.getObject();
    }

    /**
     * Attempts to create a new wrapped pooled object.
     * <p>
     * If there are {@link #getMaxTotal()} objects already in circulation
     * or in process of being created, this method returns null.
     *
     * @return The new wrapped pooled object
     *
     * @throws Exception if the object factory's {@code makeObject} fails
     */
    private PooledObject<T> create() throws Exception {
        int localMaxTotal = getMaxTotal();
        // This simplifies the code later in this method
        if (localMaxTotal < 0) {
            localMaxTotal = Integer.MAX_VALUE;
        }

        // Flag that indicates if create should:
        // - TRUE:  call the factory to create an object
        // - FALSE: return null
        // - null:  loop and re-test the condition that determines whether to
        //          call the factory
        Boolean create = null;
        while (create == null) {
            synchronized (makeObjectCountLock) {
                final long newCreateCount = createCount.incrementAndGet();
                if (newCreateCount > localMaxTotal) {
                    // The pool is currently at capacity or in the process of
                    // making enough new objects to take it to capacity.
                    createCount.decrementAndGet();
                    if (makeObjectCount == 0) {
                        // There are no makeObject() calls in progress so the
                        // pool is at capacity. Do not attempt to create a new
                        // object. Return and wait for an object to be returned
                        create = Boolean.FALSE;
                    } else {
                        // There are makeObject() calls in progress that might
                        // bring the pool to capacity. Those calls might also
                        // fail so wait until they complete and then re-test if
                        // the pool is at capacity or not.
                        makeObjectCountLock.wait();
                    }
                } else {
                    // The pool is not at capacity. Create a new object.
                    makeObjectCount++;
                    create = Boolean.TRUE;
                }
            }
        }

        if (!create.booleanValue()) {
            return null;
        }

        final PooledObject<T> p;
        try {
            // 調用指定factory的 makeObject() 方法
            p = factory.makeObject();
        } catch (final Exception e) {
            createCount.decrementAndGet();
            throw e;
        } finally {
            synchronized (makeObjectCountLock) {
                makeObjectCount--;
                makeObjectCountLock.notifyAll();
            }
        }

        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getLogAbandoned()) {
            p.setLogAbandoned(true);
        }

        createdCount.incrementAndGet();
        allObjects.put(new IdentityWrapper<T>(p.getObject()), p);
        return p;
    }
  // 使用 JedisFactory 創建一個連接到 master
  // redis.clients.jedis.JedisFactory#makeObject
  @Override
  public PooledObject<Jedis> makeObject() throws Exception {
    final HostAndPort hostAndPort = this.hostAndPort.get();
    final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
        soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);

    try {
      jedis.connect();
      // 如果存在密碼設置,則進行 auth xxx 操作
      // redis 配置: requirepass xxx
      if (null != this.password) {
        jedis.auth(this.password);
      }
      if (database != 0) {
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }
    } catch (JedisException je) {
      jedis.close();
      throw je;
    }

    return new DefaultPooledObject<Jedis>(jedis);

  }
  // redis.clients.jedis.JedisFactory#activateObject
  @Override
  public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
    final BinaryJedis jedis = pooledJedis.getObject();
    if (jedis.getDB() != database) {
      jedis.select(database);
    }

  }

  獲取到client連接后,主可以任意地通過網絡io與真實redis進行交互了。哨兵也不會成為性能問題了。

 

五、幾點思考

  哨兵模式的出現,僅為了解決單機的高可用問題,而並不會解決單機容量問題(集群模式會處理這個問題)。在當前的互聯網環境中,應用面也許沒有那么廣。但思路是值得借鑒的。

  Sentinel 在配置時只需配置master地址即可,其slave信息,sentinel信息,都是通過master來推斷的。所以,一定要確保在啟動時master是可用的,否則系統本身必須無法啟動。看起來是個脆弱的協議。

  Sentinel 的動態切換信息會寫到配置文件中去,而這個文件最初又是由管理員寫的,即動態配置與靜態混合在一起。容易讓人混淆,且容易改錯。看起來並不是那么完美。(也許設計者有其考慮吧)

  如果redis中設置了密碼,則要求必須保持全部一致,這在一定程度上會有些誤會。

  redis Sentinel 本質上是一個對等集群系統,提供服務注冊及選主服務,連接任意節點結果都是一樣的,節點間保持通過pub/sub兩兩通信。

      redis 本身就是一款高性能和高性價比的緩存產品。而sentinel為了解決一個高可用問題,帶來的額外支出並不小,這也必然會影響我們的選擇!

      市面上有很多做故障檢測和切換的工具,如nginx、keepalived、zookeeper,但都無法做到自動選主功能,因為這是應用相關性強的服務,只能是應用自身實現。但為什么不把高可用選主等功能融合到redis的服務中呢?畢竟這種功能的抽離,並沒有太多地復用性。看市面很多產品,高可用都是其自身實現的一個功能,只需做好必要配置即可,無需其他負擔。redis的哨兵架構倒是特立獨行了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM