<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 24116字,需瀏覽 49分鐘

           ·

          2021-03-09 15:13

          走過路過不要錯過

          點擊藍字關注我們


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

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

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

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

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

          一、哨兵系統(tǒng)介紹

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

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

          哨兵系統(tǒng)的架構圖如下:

          (一)服務端架構

          (二)請求處理流程圖

          二、哨兵系統(tǒng)搭建步驟

          哨兵可以搭建在 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
          # 要監(jiān)視的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 不可達持續(xù)多少毫秒后開始定義為節(jié)點下線,默認30ssentinel down-after-milliseconds mymaster 30000
          # sentinel parallel-syncs <master-name> <numslaves># 在故障轉(zhuǎn)移期間同時與新的master同步的slave數(shù)量sentinel parallel-syncs mymaster 1
          # 定義進行故障轉(zhuǎn)移的超時時間,默認3分鐘sentinel failover-timeout mymaster 180000
          # 發(fā)生故障轉(zhuǎn)移時調(diào)用的通知腳本,被調(diào)用時會傳遞兩個參數(shù): eventType, eventDescription# sentinel notification-script mymaster /var/redis/notify.sh
          # master 變更時調(diào)用腳本配置# 調(diào)用時會傳遞如下參數(shù)# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port># sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

          3. 啟動哨兵節(jié)點;

              # 使用 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 服務內(nèi)部:        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節(jié)點關閉后,等待一段時間,再獲取master地址看看。master已經(jīng)切換了。

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

          三、哨兵實現(xiàn)高可用的運行原理

          1. Sentinel 的定時任務

          • 每個 Sentinel 以每秒鐘一次的頻率向它所知的主服務器、從服務器以及其他 Sentinel 實例發(fā)送一個 PING 命令。

          • 如果一個實例(instance)距離最后一次有效回復 PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 那么這個實例會被 Sentinel 標記為主觀下線。一個有效回復可以是:+PONG 、 -LOADING 或者 -MASTERDOWN 。

          • 如果一個主服務器被標記為主觀下線, 那么正在監(jiān)視這個主服務器的所有 Sentinel 要以每秒一次的頻率確認主服務器的確進入了主觀下線狀態(tài)。

          • 如果一個主服務器被標記為主觀下線, 并且有足夠數(shù)量的 Sentinel (至少要達到配置文件指定的數(shù)量)在指定的時間范圍內(nèi)同意這一判斷, 那么這個主服務器被標記為客觀下線。

          • 在一般情況下, 每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主服務器和從服務器發(fā)送 INFO 命令。當一個主服務器被 Sentinel 標記為客觀下線時, Sentinel 向下線主服務器的所有從服務器發(fā)送 INFO 命令的頻率會從 10 秒一次改為每秒一次。

          • 當沒有足夠數(shù)量的 Sentinel 同意主服務器已經(jīng)下線, 主服務器的客觀下線狀態(tài)就會被移除。當主服務器重新向 Sentinel 的 PING 命令返回有效回復時, 主服務器的主觀下線狀態(tài)就會被移除。


          2. 自動發(fā)現(xiàn) Sentinel 和從服務器

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

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

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

          • 每個 Sentinel 會以每兩秒一次的頻率, 通過發(fā)布與訂閱功能, 向被它監(jiān)視的所有主服務器和從服務器的 sentinel:hello 頻道發(fā)送一條信息, 信息中包含了 Sentinel 的 IP 地址、端口號和運行 ID (runid)。

          • 每個 Sentinel 都訂閱了被它監(jiān)視的所有主服務器和從服務器的 sentinel:hello 頻道, 查找之前未出現(xiàn)過的 sentinel (looking for unknown sentinels)。當一個 Sentinel 發(fā)現(xiàn)一個新的 Sentinel 時, 它會將新的 Sentinel 添加到一個列表中, 這個列表保存了 Sentinel 已知的, 監(jiān)視同一個主服務器的所有其他 Sentinel 。

          • Sentinel 發(fā)送的信息中還包括完整的主服務器當前配置(configuration)。如果一個 Sentinel 包含的主服務器配置比另一個 Sentinel 發(fā)送的配置要舊, 那么這個 Sentinel 會立即升級到新配置上。

          • 在將一個新 Sentinel 添加到監(jiān)視主服務器的列表上面之前, Sentinel 會先檢查列表中是否已經(jīng)包含了和要添加的 Sentinel 擁有相同運行 ID 或者相同地址(包括 IP 地址和端口號)的 Sentinel , 如果是的話, Sentinel 會先移除列表中已有的那些擁有相同運行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。


          3. 故障轉(zhuǎn)移

          一次故障轉(zhuǎn)移操作由以下步驟組成:

          • 發(fā)現(xiàn)主服務器已經(jīng)進入客觀下線狀態(tài)。

          • 對我們的當前紀元進行自增(詳情請參考 Raft leader election ), 并嘗試在這個紀元中當選。

          • 如果當選失敗, 那么在設定的故障遷移超時時間的兩倍之后, 重新嘗試當選。如果當選成功, 那么執(zhí)行以下步驟。

          • 選出一個從服務器,并將它升級為主服務器。

          • 向被選中的從服務器發(fā)送 SLAVEOF NO ONE 命令,讓它轉(zhuǎn)變?yōu)橹鞣掌鳌?/span>

          • 通過發(fā)布與訂閱功能, 將更新后的配置傳播給所有其他 Sentinel , 其他 Sentinel 對它們自己的配置進行更新。

          • 向已下線主服務器的從服務器發(fā)送 SLAVEOF 命令, 讓它們?nèi)椭菩碌闹鞣掌鳌?/span>

          • 當所有從服務器都已經(jīng)開始復制新的主服務器時, 領頭 Sentinel 終止這次故障遷移操作。

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

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

          • 在失效主服務器屬下的從服務器當中, 那些被標記為主觀下線、已斷線、或者最后一次回復 PING 命令的時間大于五秒鐘的從服務器都會被淘汰。

          • 在失效主服務器屬下的從服務器當中, 那些與失效主服務器連接斷開的時長超過 down-after 選項指定的時長十倍的從服務器都會被淘汰。

          • 在經(jīng)歷了以上兩輪淘汰之后剩下來的從服務器中, 我們選出復制偏移量(replication offset)最大的那個從服務器作為新的主服務器;如果復制偏移量不可用, 或者從服務器的復制偏移量相同, 那么帶有最小運行 ID 的那個從服務器成為新的主服務器。

          四、客戶端使用哨兵系統(tǒng)

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

          當然,這種工作一般是要交給sdk做的,實現(xiàn)原理也差不多,我們就以 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 封裝哨兵的訪問細節(jié) 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中已經(jīng)將哨兵封裝得和普通的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發(fā)送命令請求: 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, 建立一個監(jiān)聽線程, 監(jiān)聽 sentinel 的 +switch-master 信息 // 當master發(fā)生變化時,重新初始化連接池 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 監(jiān)聽線程事務處理流程如下 // 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發(fā)生變化時,通過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寫入數(shù)據(jù)時,會先從連接池里獲取一個連接實例,其池化框架使用的是 GenericObjectPool 的通用能力,調(diào)用 JedisFactory 的 makeObject() 方法進行創(chuàng)建 :

            // redis.clients.jedis.JedisSentinelPool#getResource  @Override  public Jedis getResource() {    while (true) {      // 調(diào)用父類方法獲取實例      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 發(fā)生了切換,則將當前連接釋放,繼續(xù)嘗試獲取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) { // 沒有獲取到連接時,主動創(chuàng)建一個 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 { // 確保激活當前數(shù)據(jù)庫 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 { // 調(diào)用指定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 創(chuàng)建一個連接到 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連接后,主可以任意地通過網(wǎng)絡io與真實redis進行交互了。哨兵也不會成為性能問題了。

          五、幾點思考

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

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

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

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

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

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

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




          往期精彩推薦



          騰訊、阿里、滴滴后臺面試題匯總總結 — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內(nèi)推Java后端面試題

          JVM難學?那是因為你沒認真看完這篇文章


          END


          關注作者微信公眾號 —《JAVA爛豬皮》


          了解更多java后端架構知識以及最新面試寶典


          你點的每個好看,我都認真當成了


          看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力


          作者:等你歸去來

          出處:https://www.cnblogs.com/yougewe/p/12444375.html

          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  超碰人人爱 | 日韩中文在线字幕 | 五月天婷婷小说网 | 精品欧美一级片大片AAAA级 | AV影音在线 |