Redis 客戶端 Jedis 的特性和原理

Redis 作爲目前通用的緩存選型,因其高性能而倍受歡迎。Redis 的 2.x 版本僅支持單機模式,從 3.0 版本開始引入集羣模式。

Redis 的 Java 生態的客戶端當中包含 Jedis、Redisson、Lettuce,不同的客戶端具備不同的能力是使用方式,本文主要分析 Jedis 客戶端。

Jedis 客戶端同時支持單機模式、分片模式、集羣模式的訪問模式,通過構建 Jedis 類對象實現單機模式下的數據訪問,通過構建 ShardedJedis 類對象實現分片模式的數據訪問,通過構建 JedisCluster 類對象實現集羣模式下的數據訪問。

Jedis 客戶端支持單命令和 Pipeline 方式訪問 Redis 集羣,通過 Pipeline 的方式能夠提高集羣訪問的效率。

本文的整體分析基於 Jedis 的 3.5.0 版本進行分析,相關源碼均參考此版本。

一、Jedis 訪問模式對比

Jedis 客戶端操作 Redis 主要分爲三種模式,分表是單機模式、分片模式、集羣模式。

  • 單機模式主要是創建 Jedis 對象來操作單節點的 Redis,只適用於訪問單個 Redis 節點。
  • 分片模式(ShardedJedis)主要是通過創建 ShardedJedisPool 對象來訪問分片模式的多個 Redis 節點,是 Redis 沒有集羣功能之前客戶端實現的一個數據分佈式方案,本質上是客戶端通過一致性哈希來實現數據分佈式存儲。
  • 集羣模式(JedisCluster)主要是通過創建 JedisCluster 對象來訪問集羣模式下的多個 Redis 節點,是 Redis3.0 引入集羣模式後客戶端實現的集羣訪問訪問,本質上是通過引入槽(slot)概念以及通過 CRC16 哈希槽算法來實現數據分佈式存儲。

單機模式不涉及任何分片的思想,所以我們着重分析分片模式和集羣模式的理念。

1.1 分片模式

  • 分片模式本質屬於基於客戶端的分片,在客戶端實現如何根據一個 key 找到 Redis 集羣中對應的節點的方案。
  • Jedis 的客戶端分片模式採用一致性 Hash 來實現,一致性 Hash 算法的好處是當 Redis 節點進行增減時只會影響新增或刪除節點前後的小部分數據,相對於取模等算法來說對數據的影響範圍較小。
  • Redis 在大部分場景下作爲緩存進行使用,所以不用考慮數據丟失致使緩存穿透造成的影響,在 Redis 節點增減時可以不用考慮部分數據無法命中的問題。

分片模式的整體應用如下圖所示,核心在於客戶端的一致性 Hash 策略。


1.2 集羣模式

集羣模式本質屬於服務器分片技術,由 Redis 集羣本身提供分片功能,從 Redis 3.0 版本開始正式提供。

集羣的原理是:一個 Redis 集羣包含 16384 個哈希槽(Hash slot), Redis 保存的每個鍵都屬於這 16384 個哈希槽的其中一個, 集羣使用公式 CRC16(key)%16384 來計算鍵 key 屬於哪個槽, 其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和 。

集羣中的每個節點負責處理一部分哈希槽。舉個例子, 一個集羣可以有三個哈希槽, 其中:

  • 節點 A 負責處理 0 號至 5500 號哈希槽。
  • 節點 B 負責處理 5501 號至 11000 號哈希槽。
  • 節點 C 負責處理 11001 號至 16383 號哈希槽。

Redis 在集羣模式下對於 key 的讀寫過程首先將對應的 key 值進行 CRC16 計算得到對應的哈希值,將哈希值對槽位總數取模映射到對應的槽位,最終映射到對應的節點進行讀寫。以命令 set("key", "value")爲例子,它會使用 CRC16 算法對 key 進行計算得到哈希值 28989,然後對 16384 進行取模得到 12605,最後找到 12605 對應的 Redis 節點,最終跳轉到該節點執行 set 命令。

集羣模式的整體應用如下圖所示,核心在於集羣哈希槽的設計以及重定向命令。

二、Jedis 的基礎用法

// Jedis單機模式的訪問
public void main(String[] args) {
    // 創建Jedis對象
    jedis = new Jedis("localhost", 6379);
    // 執行hmget操作
    jedis.hmget("foobar", "foo");
    // 關閉Jedis對象
    jedis.close();
}
 
// Jedis分片模式的訪問
public void main(String[] args) {
    HostAndPort redis1 = HostAndPortUtil.getRedisServers().get(0);
    HostAndPort redis2 = HostAndPortUtil.getRedisServers().get(1);
    List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>(2);
    JedisShardInfo shard1 = new JedisShardInfo(redis1);
    JedisShardInfo shard2 = new JedisShardInfo(redis2);
    // 創建ShardedJedis對象
    ShardedJedis shardedJedis = new ShardedJedis(shards);
    // 通過ShardedJedis對象執行set操作
    shardedJedis.set("a", "bar");
}
 
// Jedis集羣模式的訪問
public void main(String[] args) {
    // 構建redis的集羣池
    Set<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("127.0.0.1", 7001));
    nodes.add(new HostAndPort("127.0.0.1", 7002));
    nodes.add(new HostAndPort("127.0.0.1", 7003));
 
    // 創建JedisCluster
    JedisCluster cluster = new JedisCluster(nodes);
 
    // 執行JedisCluster對象中的方法
    cluster.set("cluster-test", "my jedis cluster test");
    String result = cluster.get("cluster-test");
}

Jedis 通過創建 Jedis 的類對象來實現單機模式下的數據訪問,通過構建 JedisCluster 類對象來實現集羣模式下的數據訪問。

要理解 Jedis 的訪問 Redis 的整個過程,可以通過先理解單機模式下的訪問流程,在這個基礎上再分析集羣模式的訪問流程會比較合適。

三、Jedis 單機模式的訪問

Jedis 訪問單機模式 Redis 的整體流程圖如下所示,從圖中可以看出核心的流程包含 Jedis 對象的創建以及通過 Jedis 對象實現 Redis 的訪問。

熟悉 Jedis 訪問單機 Redis 的過程,本身就是需要了解 Jedis 的創建過程以及執行 Redis 命令的過程。

  • Jedis 的創建過程核心在於創建 Jedis 對象以及 Jedis 內部變量 Client 對象。
  • Jedis 訪問 Redis 的過程在於通過 Jedis 內部的 Client 對象訪問 Redis。


3.1 創建過程

Jedis 本身的類關係圖如下圖所示,從圖中我們能夠看到 Jedis 繼承自 BinaryJedis 類。

在 BinaryJedis 類中存在和 Redis 對接的 Client 類對象,Jedis 通過父類的 BinaryJedis 的 Client 對象實現 Redis 的讀寫。



Jedis 類在創建過程中通過父類 BinaryJedis 創建了 Client 對象,而瞭解 Client 對象是進一步理解訪問過程的關鍵。

public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands,
    AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands, ModuleCommands {
 
  protected JedisPoolAbstract dataSource = null;
 
  public Jedis(final String host, final int port) {
    // 創建父類BinaryJedis對象
    super(host, port);
  }
}
 
public class BinaryJedis implements BasicCommands, BinaryJedisCommands, MultiKeyBinaryCommands,
    AdvancedBinaryJedisCommands, BinaryScriptingCommands, Closeable {
 
  // 訪問redis的Client對象
  protected Client client = null;
 
  public BinaryJedis(final String host, final int port) {
    // 創建Client對象訪問redis
    client = new Client(host, port);
  }
}

Client 類的類關係圖如下圖所示,Client 對象繼承自 BinaryClient 和 Connection 類。在 BinaryClient 類中存在 Redis 訪問密碼等相關參數,在 Connection 類在存在訪問 Redis 的 socket 對象以及對應的輸入輸出流。本質上 Connection 是和 Redis 進行通信的核心類。



Client 類在創建過程中初始化核心父類 Connection 對象,而 Connection 是負責和 Redis 直接進行通信。

public class Client extends BinaryClient implements Commands {
  public Client(final String host, final int port) {
    super(host, port);
  }
}
 
public class BinaryClient extends Connection {
  // 存儲和Redis連接的相關信息
  private boolean isInMulti;
  private String user;
  private String password;
  private int db;
  private boolean isInWatch;
 
  public BinaryClient(final String host, final int port) {
    super(host, port);
  }
}
 
public class Connection implements Closeable {
  // 管理和Redis連接的socket信息及對應的輸入輸出流
  private JedisSocketFactory jedisSocketFactory;
  private Socket socket;
  private RedisOutputStream outputStream;
  private RedisInputStream inputStream;
  private int infiniteSoTimeout = 0;
  private boolean broken = false;
 
  public Connection(final String host, final int port, final boolean ssl,
      SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier) {
    // 構建DefaultJedisSocketFactory來創建和Redis連接的Socket對象
    this(new DefaultJedisSocketFactory(host, port, Protocol.DEFAULT_TIMEOUT,
        Protocol.DEFAULT_TIMEOUT, ssl, sslSocketFactory, sslParameters, hostnameVerifier));
  }
}

3.2 訪問過程

以 Jedis 執行 set 命令爲例,整個過程如下:

  • Jedis 的 set 操作是通過 Client 的 set 操作來實現的。
  • Client 的 set 操作是通過父類 Connection 的 sendCommand 來實現。
public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands,
    AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands, ModuleCommands {
  @Override
  public String set(final String key, final String value) {
    checkIsInMultiOrPipeline();
    // client執行set操作
    client.set(key, value);
    return client.getStatusCodeReply();
  }
}
 
public class Client extends BinaryClient implements Commands {
  @Override
  public void set(final String key, final String value) {
    // 執行set命令
    set(SafeEncoder.encode(key), SafeEncoder.encode(value));
  }
}
 
public class BinaryClient extends Connection {
  public void set(final byte[] key, final byte[] value) {
    // 發送set指令
    sendCommand(SET, key, value);
  }
}
 
public class Connection implements Closeable {
  public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    try {
      // socket連接redis
      connect();
      // 按照redis的協議發送命令
      Protocol.sendCommand(outputStream, cmd, args);
    } catch (JedisConnectionException ex) {
    }
  }
}

四、Jedis 分片模式的訪問

基於前面已經介紹的 Redis 分片模式的一致性 Hash 的原理來理解 Jedis 的分片模式的訪問。

關於 Redis 分片模式的概念:Redis 在 3.0 版本之前沒有集羣模式的概念,這導致單節點能夠存儲的數據有限,通過 Redis 的客戶端如 Jedis 在客戶端通過一致性 Hash 算法來實現數據的分片存儲。

本質上 Redis 的分片模式跟 Redis 本身沒有任何關係,只是通過客戶端來解決單節點數據有限存儲的問題。

ShardedJedis 訪問 Redis 的核心在於構建對象的時候初始化一致性 Hash 對象,構建一致性 Hash 經典的 Hash 值和 node 的映射關係。構建完映射關係後執行 set 等操作就是 Hash 值到 node 的尋址過程,尋址完成後直接進行單節點的操作。

4.1 創建過程

ShardedJedis 的創建過程在於父類的 Sharded 中關於一致性 Hash 相關的初始化過程,核心在於構建一致性的虛擬節點以及虛擬節點和 Redis 節點的映射關係。

源碼中最核心的部分代碼在於根據根據權重映射成未 160 個虛擬節點,通過虛擬節點來定位到具體的 Redis 節點。

public class Sharded<R, S extends ShardInfo<R>> {
 
  public static final int DEFAULT_WEIGHT = 1;
  // 保存虛擬節點和redis的node節點的映射關係
  private TreeMap<Long, S> nodes;
  // hash算法
  private final Hashing algo;
  // 保存redis節點和訪問該節點的Jedis的連接信息
  private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<>();
 
  public Sharded(List<S> shards, Hashing algo) {
    this.algo = algo;
    initialize(shards);
  }
 
  private void initialize(List<S> shards) {
    nodes = new TreeMap<>();
    // 遍歷每個redis的節點並設置hash值到節點的映射關係
    for (int i = 0; i != shards.size(); ++i) {
      final S shardInfo = shards.get(i);
      // 根據權重映射成未160個虛擬節點
      int N =  160 * shardInfo.getWeight();
      if (shardInfo.getName() == null) for (int n = 0; n < N; n++) {
        // 構建hash值和節點映射關係
        nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
      }
      else for (int n = 0; n < N; n++) {
        nodes.put(this.algo.hash(shardInfo.getName() + "*" + n), shardInfo);
      }
      // 保存每個節點的訪問對象
      resources.put(shardInfo, shardInfo.createResource());
    }
  }
}

4.2 訪問過程

ShardedJedis 的訪問過程就是一致性 Hash 的計算過程,核心的邏輯就是:通過 Hash 算法對訪問的 key 進行 Hash 計算生成 Hash 值,根據 Hash 值獲取對應 Redis 節點,根據對應的 Redis 節點獲取對應的訪問對象 Jedis。

獲取訪問對象 Jedis 之後就可以直接進行命令操作。

public class Sharded<R, S extends ShardInfo<R>> {
 
  public static final int DEFAULT_WEIGHT = 1;
  private TreeMap<Long, S> nodes;
  private final Hashing algo;
  // 保存redis節點和訪問該節點的Jedis的連接信息
  private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<>();
 
  public R getShard(String key) {
    // 根據redis節點找到對應的訪問對象Jedis
    return resources.get(getShardInfo(key));
  }
 
  public S getShardInfo(String key) {
    return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
  }
 
  public S getShardInfo(byte[] key) {
    // 針對訪問的key生成對應的hash值
    // 根據hash值找到對應的redis節點
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
    if (tail.isEmpty()) {
      return nodes.get(nodes.firstKey());
    }
    return tail.get(tail.firstKey());
  }
}

五、Jedis 集羣模式的訪問

基於前面介紹的 Redis 的集羣原理來理解 Jedis 的集羣模式的訪問。

Jedis 能夠實現 key 和哈希槽的定位的核心機制在於哈希槽和 Redis 節點的映射,而這個發現過程基於 Redis 的 cluster slot 命令。

關於 Redis 集羣操作的命令:Redis 通過 cluster slots 會返回 Redis 集羣的整體狀況。返回每一個 Redis 節點的信息包含:

  • 哈希槽起始編號
  • 哈希槽結束編號
  • 哈希槽對應 master 節點,節點使用 IP/Port 表示
  • master 節點的第一個副本
  • master 節點的第二個副本
127.0.0.1:30001> cluster slots
1) 1) (integer) 0 // 開始槽位
   2) (integer) 5460 // 結束槽位
   3) 1) "127.0.0.1" // master節點的host
      2) (integer) 30001 // master節點的port
      3) "09dbe9720cda62f7865eabc5fd8857c5d2678366" // 節點的編碼
   4) 1) "127.0.0.1" // slave節點的host
      2) (integer) 30004 // slave節點的port
      3) "821d8ca00d7ccf931ed3ffc7e3db0599d2271abf" // 節點的編碼
2) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 30002
      3) "c9d93d9f2c0c524ff34cc11838c2003d8c29e013"
   4) 1) "127.0.0.1"
      2) (integer) 30005
      3) "faadb3eb99009de4ab72ad6b6ed87634c7ee410f"
3) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 30003
      3) "044ec91f325b7595e76dbcb18cc688b6a5b434a1"
   4) 1) "127.0.0.1"
      2) (integer) 30006
      3) "58e6e48d41228013e5d9c1c37c5060693925e97e"

Jedis 訪問集羣模式 Redis 的整體流程圖如下所示,從圖中可以看出核心的流程包含 JedisCluster 對象的創建以及通過 JedisCluster 對象實現 Redis 的訪問。

JedisCluster 對象的創建核心在於創建 JedisClusterInfoCache 對象並通過集羣發現來建立 slot 和集羣節點的映射關係。

JedisCluster 對 Redis 集羣的訪問在於獲取 key 所在的 Redis 節點並通過 Jedis 對象進行訪問。

5.1 創建過程

JedisCluster 的類關係如下圖所示,在圖中可以看到核心變量 JedisSlotBasedConnectionHandler 對象。


JedisCluster 的父類 BinaryJedisCluster 創建了 JedisSlotBasedConnectionHandler 對象,該對象負責和 Redis 的集羣進行通信。

public class JedisCluster extends BinaryJedisCluster implements JedisClusterCommands,
    MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
  public JedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout,
      int maxAttempts, String password, String clientName, final GenericObjectPoolConfig poolConfig,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap hostAndPortMap) {
 
    // 訪問父類BinaryJedisCluster
    super(jedisClusterNode, connectionTimeout, soTimeout, maxAttempts, password, clientName, poolConfig,
        ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMap);
  }
}
 
public class BinaryJedisCluster implements BinaryJedisClusterCommands,
    MultiKeyBinaryJedisClusterCommands, JedisClusterBinaryScriptingCommands, Closeable {
  public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout,
      int maxAttempts, String user, String password, String clientName, GenericObjectPoolConfig poolConfig,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap hostAndPortMap) {
 
    // 創建JedisSlotBasedConnectionHandler對象
    this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
        connectionTimeout, soTimeout, user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMap);
 
    this.maxAttempts = maxAttempts;
  }
}

JedisSlotBasedConnectionHandler 的核心在於創建並初始化 JedisClusterInfoCache 對象,該對象緩存了 Redis 集羣的信息。

JedisClusterInfoCache 對象的初始化過程通過 initializeSlotsCache 來完成,主要目的用於實現集羣節點和槽位發現。

public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
  public JedisSlotBasedConnectionHandler(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig,
      int connectionTimeout, int soTimeout, String user, String password, String clientName,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
 
    super(nodes, poolConfig, connectionTimeout, soTimeout, user, password, clientName,
        ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
  }
}
 
public abstract class JedisClusterConnectionHandler implements Closeable {
  public JedisClusterConnectionHandler(Set<HostAndPort> nodes, final GenericObjectPoolConfig poolConfig,
      int connectionTimeout, int soTimeout, int infiniteSoTimeout, String user, String password, String clientName,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
      HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
 
    // 創建JedisClusterInfoCache對象
    this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, infiniteSoTimeout,
        user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
 
    // 初始化jedis的Slot信息
    initializeSlotsCache(nodes, connectionTimeout, soTimeout, infiniteSoTimeout,
        user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
  }
 
 
  private void initializeSlotsCache(Set<HostAndPort> startNodes,
      int connectionTimeout, int soTimeout, int infiniteSoTimeout, String user, String password, String clientName,
      boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier) {
    for (HostAndPort hostAndPort : startNodes) {
 
      try (Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
          soTimeout, infiniteSoTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier)) {
 
        // 通過discoverClusterNodesAndSlots進行集羣發現
        cache.discoverClusterNodesAndSlots(jedis);
        return;
      } catch (JedisConnectionException e) {
      }
    }
  }
}

JedisClusterInfoCache 的 nodes 用來保存 Redis 集羣的節點信息,slots 用來保存槽位和集羣節點的信息。

nodes 和 slots 維持的對象都是 JedisPool 對象,該對象維持了和 Redis 的連接信息。集羣的發現過程由 discoverClusterNodesAndSlots 來實現,本質是執行 Redis 的集羣發現命令 cluster slots 實現的。

public class JedisClusterInfoCache {
  // 負責保存redis集羣的節點信息
  private final Map<String, JedisPool> nodes = new HashMap<>();
  // 負責保存redis的槽位和redis節點的映射關係
  private final Map<Integer, JedisPool> slots = new HashMap<>();
 
  // 負責集羣的發現邏輯
  public void discoverClusterNodesAndSlots(Jedis jedis) {
    w.lock();
 
    try {
      reset();
      List<Object> slots = jedis.clusterSlots();
 
      for (Object slotInfoObj : slots) {
        List<Object> slotInfo = (List<Object>) slotInfoObj;
 
        if (slotInfo.size() <= MASTER_NODE_INDEX) {
          continue;
        }
        // 獲取redis節點對應的槽位信息
        List<Integer> slotNums = getAssignedSlotArray(slotInfo);
 
        // hostInfos
        int size = slotInfo.size();
        for (int i = MASTER_NODE_INDEX; i < size; i++) {
          List<Object> hostInfos = (List<Object>) slotInfo.get(i);
          if (hostInfos.isEmpty()) {
            continue;
          }
 
          HostAndPort targetNode = generateHostAndPort(hostInfos);
          // 負責保存redis節點信息
          setupNodeIfNotExist(targetNode);
          if (i == MASTER_NODE_INDEX) {
            // 負責保存槽位和redis節點的映射關係
            assignSlotsToNode(slotNums, targetNode);
          }
        }
      }
    } finally {
      w.unlock();
    }
  }
 
  public void assignSlotsToNode(List<Integer> targetSlots, HostAndPort targetNode) {
    w.lock();
    try {
      JedisPool targetPool = setupNodeIfNotExist(targetNode);
      // 保存槽位和對應的JedisPool對象
      for (Integer slot : targetSlots) {
        slots.put(slot, targetPool);
      }
    } finally {
      w.unlock();
    }
  }
 
  public JedisPool setupNodeIfNotExist(HostAndPort node) {
    w.lock();
    try {
      // 生產redis節點對應的nodeKey
      String nodeKey = getNodeKey(node);
      JedisPool existingPool = nodes.get(nodeKey);
      if (existingPool != null) return existingPool;
      // 生產redis節點對應的JedisPool
      JedisPool nodePool = new JedisPool(poolConfig, node.getHost(), node.getPort(),
          connectionTimeout, soTimeout, infiniteSoTimeout, user, password, 0, clientName,
          ssl, sslSocketFactory, sslParameters, hostnameVerifier);
      // 保存redis節點的key和對應的JedisPool對象
      nodes.put(nodeKey, nodePool);
      return nodePool;
    } finally {
      w.unlock();
    }
  }
}

JedisPool 的類關係如下圖所示,其中內部 internalPool 是通過 apache common pool 來實現的池化。



JedisPool 內部的 internalPool 通過 JedisFactory 的 makeObject 來創建 Jedis 對象。

每個 Redis 節點都會對應一個 JedisPool 對象,通過 JedisPool 來管理 Jedis 的申請釋放複用等。

public class JedisPool extends JedisPoolAbstract {
 
  public JedisPool() {
    this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
  }
}
 
public class JedisPoolAbstract extends Pool<Jedis> {
 
  public JedisPoolAbstract() {
    super();
  }
}
 
public abstract class Pool<T> implements Closeable {
  protected GenericObjectPool<T> internalPool;
 
  public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
    if (this.internalPool != null) {
      try {
        closeInternalPool();
      } catch (Exception e) {
      }
    }
    this.internalPool = new GenericObjectPool<>(factory, poolConfig);
  }
}
 
class JedisFactory implements PooledObjectFactory<Jedis> {
   
  @Override
  public PooledObject<Jedis> makeObject() throws Exception {
    // 創建Jedis對象
    final HostAndPort hp = this.hostAndPort.get();
    final Jedis jedis = new Jedis(hp.getHost(), hp.getPort(), connectionTimeout, soTimeout,
        infiniteSoTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
 
    try {
      // Jedis對象連接
      jedis.connect();
      if (user != null) {
        jedis.auth(user, password);
      } else if (password != null) {
        jedis.auth(password);
      }
      if (database != 0) {
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }
    } catch (JedisException je) {
      jedis.close();
      throw je;
    }
    // 將Jedis對象包裝成DefaultPooledObject進行返回
    return new DefaultPooledObject<>(jedis);
  }
}

5.1 訪問過程

JedisCluster 訪問 Redis 的過程通過 JedisClusterCommand 來實現重試機制,最終通過 Jedis 對象來實現訪問。從實現的角度來說 JedisCluster 是在 Jedis 之上封裝了一層,進行集羣節點定位以及重試機制等。

以 set 命令爲例,整個訪問通過 JedisClusterCommand 實現如下:

  • 計算 key 所在的 Redis 節點。
  • 獲取 Redis 節點對應的 Jedis 對象。
  • 通過 Jedis 對象進行 set 操作。
public class JedisCluster extends BinaryJedisCluster implements JedisClusterCommands,
    MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
 
  @Override
  public String set(final String key, final String value, final SetParams params) {
    return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
      @Override
      public String execute(Jedis connection) {
        return connection.set(key, value, params);
      }
    }.run(key);
  }
}

JedisClusterCommand 的 run 方法核心主要定位 Redis 的 key 所在的 Redis 節點,然後獲取與該節點對應的 Jedis 對象進行訪問。

在 Jedis 對象訪問異常後,JedisClusterCommand 會進行重試操作並按照一定策略執行 renewSlotCache 方法進行重集羣節點重發現動作。

public abstract class JedisClusterCommand<T> {
  public T run(String key) {
    // 針對key進行槽位的計算
    return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
  }
   
  private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
 
    Jedis connection = null;
    try {
 
      if (redirect != null) {
        connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
        if (redirect instanceof JedisAskDataException) {
          connection.asking();
        }
      } else {
        if (tryRandomNode) {
          connection = connectionHandler.getConnection();
        } else {
          // 根據slot去獲取Jedis對象
          connection = connectionHandler.getConnectionFromSlot(slot);
        }
      }
      // 執行真正的Redis的命令
      return execute(connection);
    } catch (JedisNoReachableClusterNodeException jnrcne) {
      throw jnrcne;
    } catch (JedisConnectionException jce) {
 
      releaseConnection(connection);
      connection = null;
 
      if (attempts <= 1) {
        // 保證最後兩次機會去重新刷新槽位和節點的對應的信息
        this.connectionHandler.renewSlotCache();
      }
      // 按照重試次數進行重試操作
      return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
    } catch (JedisRedirectionException jre) {
      // 針對返回Move命令立即觸發重新刷新槽位和節點的對應信息
      if (jre instanceof JedisMovedDataException) {
        // it rebuilds cluster's slot cache recommended by Redis cluster specification
        this.connectionHandler.renewSlotCache(connection);
      }
 
      releaseConnection(connection);
      connection = null;
 
      return runWithRetries(slot, attempts - 1, false, jre);
    } finally {
      releaseConnection(connection);
    }
  }
}

JedisSlotBasedConnectionHandler 的 cache 對象維持了 slot 和 node 的映射關係,通過 getConnectionFromSlot 方法來獲取該 slot 對應的 Jedis 對象。

public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
 
  protected final JedisClusterInfoCache cache;
 
  @Override
  public Jedis getConnectionFromSlot(int slot) {
    // 獲取槽位對應的JedisPool對象
    JedisPool connectionPool = cache.getSlotPool(slot);
    if (connectionPool != null) {
      // 從JedisPool對象中獲取Jedis對象
      return connectionPool.getResource();
    } else {
      // 獲取失敗就重新刷新槽位信息
      renewSlotCache();
      connectionPool = cache.getSlotPool(slot);
      if (connectionPool != null) {
        return connectionPool.getResource();
      } else {
        //no choice, fallback to new connection to random node
        return getConnection();
      }
    }
  }
}

六、 Jedis 的 Pipeline 實現

Pipeline 的技術核心思想是將多個命令發送到服務器而不用等待回覆,最後在一個步驟中讀取該答覆。這種模式的好處在於節省了請求響應這種模式的網絡開銷。

Redis 的普通命令如 set 和 Pipeline 批量操作的核心的差別在於 set 命令的操作會直接發送請求到 Redis 並同步等待結果返回,而 Pipeline 的操作會發送請求但不立即同步等待結果返回,具體的實現可以從 Jedis 的源碼一探究竟。

原生的 Pipeline 在集羣模式下相關的 key 必須 Hash 到同一個節點才能生效,原因在於 Pipeline 下的 Client 對象只能其中的一個節點建立了連接。

在集羣模式下歸屬於不同節點的 key 能夠使用 Pipeline 就需要針對每個 key 保存對應的節點的 client 對象,在最後執行獲取數據的時候一併獲取。本質上可以認爲在單節點的 Pipeline 的基礎上封裝成一個集羣式的 Pipeline。

6.1 Pipeline 用法分析

Pipeline 訪問單節點的 Redis 的時候,通過 Jedis 對象的 Pipeline 方法返回 Pipeline 對象,其他的命令操作通過該 Pipeline 對象進行訪問。

Pipeline 從使用角度來分析,會批量發送多個命令並最後統一使用 syncAndReturnAll 來一次性返回結果。

public void pipeline() {
    jedis = new Jedis(hnp.getHost(), hnp.getPort(), 500);
    Pipeline p = jedis.pipelined();
    // 批量發送命令到redis
    p.set("foo", "bar");
    p.get("foo");
    // 同步等待響應結果
    List<Object> results = p.syncAndReturnAll();
 
    assertEquals(2, results.size());
    assertEquals("OK", results.get(0));
    assertEquals("bar", results.get(1));
 }
 
 
public abstract class PipelineBase extends Queable implements BinaryRedisPipeline, RedisPipeline {
 
  @Override
  public Response<String> set(final String key, final String value) {
    // 發送命令
    getClient(key).set(key, value);
    // pipeline的getResponse只是把待響應的請求聚合到pipelinedResponses對象當中
    return getResponse(BuilderFactory.STRING);
  }
}
 
 
public class Queable {
 
  private Queue<Response<?>> pipelinedResponses = new LinkedList<>();
  protected <T> Response<T> getResponse(Builder<T> builder) {
    Response<T> lr = new Response<>(builder);
    // 統一保存到響應隊列當中
    pipelinedResponses.add(lr);
    return lr;
  }
}
 
 
public class Pipeline extends MultiKeyPipelineBase implements Closeable {
 
  public List<Object> syncAndReturnAll() {
    if (getPipelinedResponseLength() > 0) {
      // 根據批量發送命令的個數即需要批量返回命令的個數,通過client對象進行批量讀取
      List<Object> unformatted = client.getMany(getPipelinedResponseLength());
      List<Object> formatted = new ArrayList<>();
      for (Object o : unformatted) {
        try {
          // 格式化每個返回的結果並最終保存在列表中進行返回
          formatted.add(generateResponse(o).get());
        } catch (JedisDataException e) {
          formatted.add(e);
        }
      }
      return formatted;
    } else {
      return java.util.Collections.<Object> emptyList();
    }
  }
}

普通 set 命令發送請求給 Redis 後立即通過 getStatusCodeReply 來獲取響應結果,所以這是一種請求響應的模式。

getStatusCodeReply 在獲取響應結果的時候會通過 flush()命令強制發送報文到 Redis 服務端然後通過讀取響應結果。

public class BinaryJedis implements BasicCommands, BinaryJedisCommands, MultiKeyBinaryCommands,
    AdvancedBinaryJedisCommands, BinaryScriptingCommands, Closeable {
 
  @Override
  public String set(final byte[] key, final byte[] value) {
    checkIsInMultiOrPipeline();
    // 發送命令
    client.set(key, value);
    // 等待請求響應
    return client.getStatusCodeReply();
  }
}
 
 
public class Connection implements Closeable {
  public String getStatusCodeReply() {
    // 通過flush立即發送請求
    flush();
    // 處理響應請求
    final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
    if (null == resp) {
      return null;
    } else {
      return SafeEncoder.encode(resp);
    }
  }
}
 
 
public class Connection implements Closeable {
  protected void flush() {
    try {
      // 針對輸出流進行flush操作保證報文的發出
      outputStream.flush();
    } catch (IOException ex) {
      broken = true;
      throw new JedisConnectionException(ex);
    }
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章