Jedis學習筆記

一、Jedis介紹

Jedis是Redis官方推薦的Java連接開發工具。
Jedis的基本使用非常簡單,只需要創建Jedis對象的時候指定host,port, password即可。

Jedis jedis = new Jedis("localhost", 6379);  //指定Redis服務Host和port
jedis.auth("xxxx"); //如果Redis服務連接需要密碼,制定密碼
String value = jedis.get("key"); //訪問Redis服務
jedis.close(); //使用完關閉連接

Jedis基本使用十分簡單,在每次使用時,構建Jedis對象即可。在Jedis對象構建好之後,Jedis底層會打開一條Socket通道和Redis服務進行連接。所以在使用完Jedis對象之後,需要調用Jedis.close()方法把連接關閉,不如會佔用系統資源。

當然,如果應用非常頻繁的創建和銷燬Jedis對象,對應用的性能是很大影響的,因爲構建Socket的通道是很耗時的(類似數據庫連接)。我們應該使用連接池來減少Socket對象的創建和銷燬過程。

二、Jedis的連接池

Jedis連接池是基於apache-commons pool2實現的。在構建連接池對象的時候,需要提供池對象的配置對象,及JedisPoolConfig(繼承自GenericObjectPoolConfig)。我們可以通過這個配置對象對連接池進行相關參數的配置(如最大連接數,最大空閒連接數等)。

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(8);
config.setMaxTotal(18);
JedisPool pool = new JedisPool(config, "127.0.0.1", 6379, 2000, "password");
Jedis jedis = pool.getResource();
String value = jedis.get("key");
......
jedis.close();
pool.close();

使用Jedis連接池之後,在每次用完連接對象後一定要記得把連接歸還給連接池。Jedis對close方法進行了改造,如果是連接池中的連接對象,調用Close方法將會是把連接對象返回到對象池,若不是則關閉連接。可以查看如下代碼

@Override
public void close() { //Jedis的close方法
    if (dataSource != null) {
        if (client.isBroken()) {
            this.dataSource.returnBrokenResource(this);
        } else {
            this.dataSource.returnResource(this);
        }
    } else {
        client.close();
    }
}

//另外從對象池中獲取Jedis鏈接時,將會對dataSource進行設置
// JedisPool.getResource()方法
public Jedis getResource() {
    Jedis jedis = super.getResource();   
    jedis.setDataSource(this);
    return jedis;

三、高可用連接

我們知道,連接池可以大大提高應用訪問Reids服務的性能,減去大量的Socket的創建和銷燬過程。但是Redis爲了保障高可用,服務一般都是Sentinel部署方式。當Redis服務中的主服務掛掉之後,會仲裁出另外一臺Slaves服務充當Master。這個時候,我們的應用即使使用了Jedis連接池,Master服務掛了,我們的應用獎還是無法連接新的Master服務。爲了解決這個問題,Jedis也提供了相應的Sentinel實現,能夠在Redis Sentinel主從切換時候,通知我們的應用,把我們的應用連接到新的 Master服務。先看下怎麼使用。

Set<String> sentinels = new HashSet<>();
sentinels.add("172.18.18.207:26379");
sentinels.add("172.18.18.208:26379");
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(5);
config.setMaxTotal(20);
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, config);
Jedis jedis = pool.getResource();
jedis.set("jedis", "jedis");
......
jedis.close();
pool.close();

Jedis Sentinel的使用也是十分簡單的,只是在JedisPool中添加了Sentinel和MasterName參數。Jedis Sentinel底層基於Redis訂閱實現Redis主從服務的切換通知。當Reids發生主從切換時,Sentinel會發送通知主動通知Jedis進行連接的切換。JedisSentinelPool在每次從連接池中獲取鏈接對象的時候,都要對連接對象進行檢測,如果此鏈接和Sentinel的Master服務連接參數不一致,則會關閉此連接,重新獲取新的Jedis連接對象。

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());
        if (master.equals(connection)) {
            // connected to the correct master
            return jedis;
        } else {
            returnBrokenResource(jedis);
        }
    }
}

當然,JedisSentinelPool對象要時時監控RedisSentinel的主從切換。在其內部通過Reids的訂閱實現。具體的實現看JedisSentinelPool的兩個方法就很清晰

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
    HostAndPort master = null;
    boolean sentinelAvailable = false;
    log.info("Trying to find master from available Sentinels...");
    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());
            //從RedisSentinel中獲取Master信息
            List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
            sentinelAvailable = true; // connected to sentinel...
            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) {
            // 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...");
    //啓動後臺線程監控RedisSentinal的主從切換通知
    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;
}


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);
    }
}

可以看到,JedisSentinel的監控時使用MasterListener這個對象來實現的。看對應源碼可以發現是基於Redis的訂閱實現的,其訂閱頻道爲"+switch-master"。當MasterListener接收到switch-master消息時候,會使用新的Host和port進行initPool。這樣對連接池中的連接對象清除,重新創建新的連接指向新的Master服務。

四、Redis Cluster與JedisCluster

Redis Cluster 將全部的鍵空間劃分爲16384塊,每一塊空間稱之爲槽(slot),又將這些槽及槽所對應的 k-v 劃分給集羣中的每個主節點負責。

key -> slot 的算法選擇上,Redis Cluster 選擇的算法是 hash(key) mod 16383,即使用CRC16算法對key進行hash,然後再對16383取模,結果便是對應的slot。

常見的數據分區方法:Redis Cluster使用了虛擬槽分區
1、節點取餘分區:對特定數據取hash值再對節點數取餘來決定映射到哪一個節點。優點是簡單,缺點是擴容或收縮時需重新計算映射結果,極端情況下會導致數據全量遷移。
2、一致性哈希分區:給每個節點分配一個0~2^32的token,使其構成一個環,數據命中規則爲根據key的hash值,順時針找到第一個token大於等於該hash的節點。優點是加減節點隻影響相鄰的節點,缺點是節點少的時候優點變缺點,反倒會影響環中大部分數據,同時加減節點時候會導致部分數據無法命中。
3、虛擬槽分區:使用分散度良好的hash函數將數據映射到一個固定範圍的整數集合,這些整數便是槽位,再分給具體的節點管理。Redis Cluster使用的便是虛擬槽分區。

4.1、客戶端數據讀寫

實際上客戶端是如何讀寫數據的呢?Redis Cluster 採用了直接節點的方式。集羣模式下,客戶端去操作集羣是直連到一個具體的節點上操作的。當該節點接收到任何鍵操作命令時,會先計算鍵對應的slot,然後根據slot找出對應節點。

如果對應的節點是自身,則執行鍵操作命令,返回結果;如果不是自身,會返回給客戶端MOVED重定向錯誤,告訴客戶端應該請求具體哪個節點,由客戶端發起二次請求到正確的節點,完成本次鍵操作。

當使用redis-cli 直連集羣中節點時,使用 -c 參數,redis-cli會自動重定向連接到目標節點進行鍵操作。需要注意的是,這個自動重定向功能是redis-cli實現的,跟redis節點本身無關,節點本身依舊返回了MOVED錯誤給客戶端。

在鍵操作命令中,除了對單個鍵值的操作,還有多鍵值以及批量操作。Redis 集羣實現了所有在非分佈式版本中出現的處理單一鍵值的命令,但是在使用多個鍵值的操作,由於集羣跟客戶端的通信方式是直連節點,對於多鍵的操作卻是需要遍歷所有節點,因此是不支持的,一般由客戶端在代碼中實現需要的功能。

對於批量操作,一方面可以由客戶端代碼計算槽位,針對單個節點進行分檔,最後批量操作,另一方面,Redis Cluster 提供了hashtag 的功能,通過爲key打上hashtag,讓一類key在存儲時就位於同一個slot,達到存儲於同一個節點的效果。

hashtag: 是Cluster爲了滿足用戶讓特定Key綁定到特定槽位的需求而實現的一個功能。在計算key的slot時,如果key中包括花括號{},並且花括號中內容不爲空,便會計算花括號中標誌對應的slot。如果不包括{}或是其中內容爲空,則計算整個key對應的slot。可以利用這個功能,在特定需求中將一類key綁定到一個槽位上,但不可濫用,畢竟本身數據是分區存的,全這麼搞會導致各節點內存佔用不平衡,影響集羣性能。

注意:lua腳本執行、事務中key操作,前提都是所涉及的key在一個節點上,如果在使用集羣時無法避免這些操作,可以考慮使用hashtag,然後客戶端通過這臺節點的連接去操作。

4.2、節點間的信息共享

集羣中會有多個節點,每個節點負責一部分slot以及對應的k-v數據,並且通過直連具體節點的方式與客戶端通信。那麼問題來了,你向我(直連的節點)這裏請求一個key的value,這個key對應的slot並不歸我負責,但我又要需要告訴你MOVED到目標節點,我如何知道這個目標節點是誰呢?

Redis Cluster使用Gossip協議維護節點的元數據信息,這種協議是P2P模式的,主要職責就是信息交換。節點間不停地去交換彼此的元數據信息,那麼總會在一段時間後,大家都知道彼此是誰,負責哪些數據,是否正常工作等等。節點間信息交換是依賴於彼此發出的Gossip消息的。常用的一般是以下四種消息:
1、meet消息: 會通知接收該消息的節點,發送節點要加入當前集羣,接收者進行響應。
2、ping消息: 是集羣中的節點定期向集羣中其他節點(部分或全部)發送的連接檢測以及信息交換請求,消息包含發送節點信息以及發送節點知道的其他節點信息。
3、pong消息:是在節點接收到meet、ping消息後回覆給發送節點的響應消息,告訴發送方本次通信正常,消息包含當前節點狀態。
4、fail消息: 是在節點認爲集羣內另外某一節點下線後向集羣內所有節點廣播的消息。

在集羣啓動的過程中,有一個重要的步驟是節點握手,其本質就是在一個節點上向其他所有節點發送meet消息,消息中包含當前節點的信息(節點id,負責槽位,節點標識等等),接收方會將發送節點信息存儲至本地的節點列表中。消息體中還會包含與發送節點通信的其他節點信息(節點標識、節點id、節點ip、port等),接收方也會解析這部分內容,如果本地節點列表中不存在,則會主動向新節點發送meet消息。接收方處理完消息後,也會回覆pong消息給發送者節點,發送者也會解析pong消息更新本地存儲節點信息。因此,雖然只是在一個節點向其他所有節點發送meet消息,最後所有節點都會有其他所有節點的信息。

集羣啓動後,集羣中各節點也會定時往其他部分節點發送ping消息,用來檢測目標節點是否正常以及發送自己最新的節點負槽位信息。接收方同樣響應pong消息,由發送方更新本地節點信息。當在與某一節點通信失敗(故障發現策略後面會說)時,則會主動向集羣內節點廣播fail消息。考慮到頻繁地交換信息會加重帶寬(集羣節點越多越明顯)和計算的負擔,Redis Cluster內部的定時任務每秒執行10次,每次遍歷本地節點列表,對最近一次接受到pong消息時間大於cluster_node_timeout/2的節點立馬發送ping消息,此外每秒隨機找5個節點,選裏面最久沒有通信的節點發送ping消息。同時 ping 消息的消息投攜帶自身節點信息,消息體只會攜帶1/10的其他節點信息,避免消息過大導致通信成本過高。

cluster_node_timeout 參數影響發送消息的節點數量,調整要綜合考慮故障轉移、槽信息更新、新節點發現速度等方面。一般帶寬資源特別緊張時,可以適當調大一點這個參數,降低通信成本。

4.3、槽位遷移與集羣伸縮

Redis Cluster 支持在集羣正常服務過程中,下線或是新增集羣節點。但無論是集羣擴容還是收縮,本質上都是槽及其對應數據在不同節點上的遷移。一般情況下,槽遷移完成後,每個節點負責的槽數量基本上差不多,保證數據分佈滿足理論上的均勻。

常用的有關槽的命令如下:

  • CLUSTER ADDSLOTS slot1 [slot2]…[slotN] —— 爲當前節點分配要負責的槽,一般用於集羣創建過程。
  • CLUSTER DELSLOTS slot1 [slot2]…[slotN] —— 將特定槽從當前節點的責任區移除,和ADDSLOTS命令一樣,執行成功後會通過節點間通信將最新的槽位信息向集羣內其他節點傳播。
  • CLUSTER SETSLOT slotNum NODE nodeId —— 給指定ID的節點指派槽,一般遷移完成後在各主節點上執行,告知各主節點遷移完成。
  • CLUSTER SETSLOT slotNum IMPORTING sourceNodeId —— 在槽遷移的目標節點上執行該命令,意思是這個槽將由原節點遷移至當前節點,遷移過程中,當前節點(即目標節點)只會接收asking命令連接後的被設爲IMPORTING狀態的slot的命令。
  • CLUSTER SETSLOT slotNum MIGRATING targetNodeId —— 在槽遷移的原節點上執行該命令,意思是這個槽將由當前節點遷移至目標節點,遷移過程中,當前節點(即原節點)依舊會接受設爲MIGRATING的slot相關的請求,若具體的key依舊存在於當前節點,則處理返回結果,若不在,則返回一個帶有目標節點信息的ASK重定向錯誤。其他節點在接受到該槽的相關請求時,依舊會返回到原節點的MOVED重定向異常。

實際上遷移槽的核心是將槽對應的k-v數據遷移到目標節點。所以在完成slot在原節點和目標節點上狀態設置(即上面最後兩條命令)後,就要開始進行具體key的遷移。

  • CLUSTER GETKEYSINSLOT slot total —— 該命令返回指定槽指定個數的key集合
  • MIGRATE targetNodeIp targetNodePort key dbId timeout [auth password] —— 該命令在原節點執行,會連接到目標節點,將key及其value序列化後發送過去,在收到目標節點返回的ok後,刪除當前節點上存儲的key。整個操作是原子性的。由於集羣模式下使用各節點的0號db,所以遷移時dbId這個參數只能是0。
  • MIGRATE targetNodeIp targetNodePort “” 0 timeout [auth password] keys key1 key2… —— 該命令是上面遷移命令基於pipeline的批量版本。

在整個slot的key遷移完成後,需要在各主節點分別執行CLUSTER SETSLOT slotNum NODE nodeId來通知整個slot遷移完成。redis-trib.rb 提供的reshard功能便是基於官方提供的上述命令實現的。

集羣的擴展過程實際上就是啓動一個新節點,加入集羣(通過gossip協議進行節點握手、通信),最後從之前各節點上遷移部分slot到新節點上。

集羣的收縮過程除了除了將待下線節點的槽均勻遷移到其他主節點之外,還有對節點的下線操作。官方提供了CLUSTER FORGET downNodeId命令,用於在其他節點上執行以忘記下線節點,不與其交換信息,需要注意的是該命令有效期爲60s,超過時間後會恢復通信。一般建議使用redis-trib.rb 提供的del-node功能。

4.4、主從高可用

要保證高可用的前提是離不開從節點的,一旦某個主節點因爲某種原因不可用後,就需要一個一直默默當備胎的從節點頂上來了。

一般在集羣搭建時最少都需要6個實例,其中3個實例做主節點,各自負責一部分槽位,另外3個實例各自對應一個主節點做其從節點,對主節點的操作進行復制。Redis Cluster在給主節點添加從節點時,不支持slaveof命令,而是通過在從節點上執行命令cluster replicate masterNodeId 。

Cluster的故障發現也是基於節點通信的。每個節點在本地存儲有一個節點列表(其他節點信息),列表中每個節點元素除了存儲其ID、ip、port、狀態標識(主從角色、是否下線等等)外,還有最後一次向該節點發送ping消息的時間、最後一次接收到該節點的pong消息的時間以及一個保存其他節點對該節點下線傳播的報告鏈表。

節點與節點間會定時發送ping消息,彼此響應pong消息,成功後都會更新這個時間。同時每個節點都有定時任務掃描本地節點列表裏這兩個消息時間,若發現pong響應時間減去ping發送時間超過cluster-node-timeout配置時間(默認15秒,該參數用來設置節點間通信的超時時間)後,便會將本地列表中對應節點的狀態標識爲PFAIL,認爲其有可能下線。

節點間通信(ping)時會攜帶本地節點列表中部分節點信息,如果其中包括標記爲PFAIL的節點,那麼在消息接收方解析到該節點時,會找自己本地的節點列表中該節點元素的下線報告鏈表,看是否已經存在發送節點對於該故障節點的報告,如果有,就更新接收到發送ping消息節點對於故障節點的報告的時間,如果沒有,則將本次報告添加進鏈表。下線報告鏈表的每個元素結構只有兩部分內容,一個是報告本地這個故障節點的發送節點信息,一個是本地接收到該報告的時間(存儲該時間是因爲故障報告是有有效期的,避免誤報)。由於每個節點的下線報告鏈表都存在於各自的信息結構中,所以在瀏覽本地節點列表中每個節點元素時,可以清晰地知道,有其他哪些節點跟我說,兄弟,你正在看的這個節點我覺的涼涼了。

故障報告的有效期是 cluster-node-timeout * 2

消息接收方解析到PFAIL節點,並且更新本地列表中對應節點的故障報告鏈表後,會去查看該節點的故障報告鏈表中有效的報告節點是否超過所有主節點數的一半。如果沒超過,便繼續解析ping消息;如果超過,代表超過半數的節點認爲這個節點可能下線了,當前節點就會將PFAIL節點本地的節點信息中的狀態標識標記爲FAIL,然後向集羣內廣播一條fail消息,集羣內的所有節點接收到該fail消息後,會把各自本地節點列表中該節點的狀態標識修改爲FAIL。在所有節點對其標記未FAIL後,該FAIL節點對應的從節點就會發起轉正流程。在轉正流程完成後,這個節點就會正式下線,等到其恢復後,發現自己的槽已經被分給某個節點,便會將自己轉換成這個節點的從節點並且ping集羣內其他節點,其他節點接到恢復節點的ping消息後,便會更新其狀態標識。此外,恢復的節點若發現自己的槽還是由自己負責,就會跟其他節點通信,其他主節點發現該節點恢復後,就會拒絕其從節點的選舉,最終清除自己的FAIL狀態。

4.5、從節點升爲主節點過程

在集羣中若是某個主節點發生故障,被其他主節點標記爲FAIL狀態,爲了集羣的正常使用,這時會由其對應的從節點中晉升一個爲新的主節點,負責原主節點的一切工作。

並不是所有從節點都有被提名的資格,這個跟普通職員的晉升一樣。只有從節點與主節點的連接斷線不超過一定時間,纔會初步具備被提名的資格。該時間一般爲cluster-node-timeout *10,10是從節點的默認有效因子。

一般來說,故障主節點會有多個符合晉升要求的從節點,那麼怎麼從這些從節點中選出一個最合適的來晉升爲主節點恢復工作呢?從節點的作用是作爲主節點的備份,每個對於主節點的操作都會異步在多個從節點上備份,但受具體的主從節點結構決定,一般每個從節點對於主節點的同步程度是不同的。爲了能更好的替代原主節點工作,就必須從這些從節點中選舉一個最接近甚至完全同步主節點數據的從節點來完成最終晉升。

從節點晉升的發起點是從節點。從節點在定時任務中與其他節點通信,當發現主節點FAIL後,會判斷資深是否有晉升提名資格。如果有的話,則會根據相關規則設置一個選舉自己的時間。在到達那個設置的時間點後,再發起針對自己晉升的選舉流程,選票則由集羣中其他正常主節點選投。若自己獲得的選票超過正常主節點數的一半時,則會執行替換原主節點工作,完成本次選舉晉升。

設置選舉時間規則:發現主節點FAIL後並不會立馬發起選舉。而是經過 固定延時(500ms)+ 隨機延時(0-500ms)+ 從節點複製偏移量排名1000ms 後發起針對自己的選舉流程。其中 固定延時 是保證主節點的FAIL狀態被所有主節點獲知,隨機延時是爲了儘量避免發生多個從節點同時發起選舉的情況,最後的排名1000ms是爲了保證複製偏移量最大也就是最接近於原主節點數據的從節點最先發起選舉。因此一般來說,從節點晉升選舉一次就會成功。主節點是沒有區分哪個從節點是最適合晉升的規則的,主要靠這裏的選舉發起時間來讓最合適的一次成功。

從節點發起選舉主要分爲兩步:

  • 自增集羣的全局配置紀元,並更新爲當前節點的epoch(配置紀元這裏不詳細介紹,不懂的可以先簡單理解爲版本號,每個節點都有自己的epoch並且集羣有一個全局的epoch);

  • 向集羣內廣播選舉消息FAILOVER_AUTH_REQUEST,消息內會包含當前節點的epoch。

從節點廣播選舉消息後,在NODE_TIMEOUT*2時間內等待主節點的響應FAILOVER_AUTH_ACK。若收到大多數主節點的響應,代表選舉成功,則會通過ping\pong消息來宣誓主權。若未收到足夠響應則會中斷本次選舉,由其他節點重新發起選舉。

主節點在每個全局配置紀元中有且只有一張選票,一旦投給某個從節點便會忽視其他節點的選舉消息。一般同一個配置紀元多個從節點競爭的情況只有極小概率會發生,這是由從節點的選舉時間以及選舉步驟決定的。主節點的投票響應FAILOVER_AUTH_ACK消息中會返回接收到的選舉消息一樣的epoch,從節點也只會認可跟節點當前epoch一致的投票響應,這樣可以避免因爲網絡延遲等因素導致認可遲來的歷史認可消息。

從節點成功晉升後,在替換原主節點時,還需要進行最後三步:

  • 取消當前節點的複製工作,變身爲主節點;
  • 撤銷原主節點負責的槽,並把這些槽委派給自己;
  • 廣播pong消息,通知所有節點自己已經完成轉正以及轉正後負責的槽信息。

4.6、JedisCluster

Jedis是redis的java客戶端,JedisCluster則是Jedis根據Redis集羣的特性提供的集羣客戶端。

上文介紹過了redis集羣下操作key的詳細流程,一般通過redis-cli啓動客戶端連接具體的節點時,要操作的key若不在這個節點上時,服務端會返回MOVED重定向錯誤,這時需要手動連接至重定向節點才能繼續操作。或者redis-cli連接服務節點時加上-c 參數,就可以使用redis-cli提供的自動重定向機制,在操作其他服務節點的key時會進行自動重定向,避免客戶端手動重定向。JedisCluster作爲操作Redis集羣的java客戶端,同樣遵守RedisCluster提供的客戶端連接規範,本節從源碼的角度去看其具體是怎麼做的。

4.6.1、初始化工作

無論你使用spring集成jedis或是直接使用jedis,第一步都是客戶端的初始化工作,這裏直接從JedisCluster着手去看。JedisCluster實際上是一個高級客戶端,它繼承了BinaryJedisCluster,客戶端的初始化工作實際上都是由該類負責,此外還實現了JedisCommands、MultiKeyJedisClusterCommands和JedisClusterScriptingCommands三個接口,封裝了單鍵命令、多鍵操作命令以及腳本執行命令等具體的方法供開發人員調用。

JedisCluster的構造器有很多,但最終都是調用了父類BinaryJedisCluster的構造,實際上這裏是初始化了一個連接處理器,並且設置了最大重試次數。

public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, 
int connectionTimeout, int soTimeout, int maxAttempts, 
String password, GenericObjectPoolConfig poolConfig) {
 
  this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
          connectionTimeout, soTimeout, password);
  this.maxAttempts = maxAttempts; 
}

JedisSlotBasedConnectionHandler實際上又調用了父類JedisClusterConnectionHandler 的構造器,而這裏纔是JedisCluster初始化的核心。

public JedisClusterConnectionHandler(Set<HostAndPort> nodes,
                                     final GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password) {
  
  // 創建集羣信息的緩存對象
  this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password);
 
  // 初始化連接池與緩存信息
  initializeSlotsCache(nodes, poolConfig, password);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章