Deeply analyze the characteristics and principles of Redis client Jedis

Keywords: Database Jedis Redis cluster

1, Opening

Redis, as a general cache model, is very popular because of its high performance. Redis version 2.x only supports stand-alone mode, and the cluster mode has been introduced since version 3.0.

Redis's Java ecological clients include Jedis, Redisson and lattice. Different clients have different capabilities and use methods. This paper mainly analyzes Jedis clients.

The Jedis client supports the access modes of stand-alone mode, fragment mode and cluster mode at the same time. The data access in stand-alone mode is realized by building Jedis class objects, the data access in fragment mode is realized by building ShardedJedis class objects, and the data access in cluster mode is realized by building JedisCluster class objects.

Jedis client supports single command and Pipeline access to Redis cluster, which can improve the efficiency of cluster access.

The overall analysis of this paper is based on Jedis version 3.5.0, and the relevant source codes refer to this version.

2, Jedis access mode comparison

The Jedis client operation Redis is mainly divided into three modes: stand-alone mode, fragment mode and cluster mode.

  • The stand-alone mode is mainly to create Jedis objects to operate single node Redis. It is only applicable to accessing a single Redis node.
  • Sharded jedis mainly used to access multiple Redis nodes in sharded mode by creating sharded jedispool objects. It is a data distribution scheme implemented by the client before Redis has no cluster function. In essence, the client realizes data distributed storage through consistent hash.
  • Cluster mode (JedisCluster) mainly accesses multiple Redis nodes in cluster mode by creating JedisCluster objects. It is a cluster access realized by clients after Redis3.0 introduces cluster mode. In essence, it realizes data distributed storage by introducing the concept of slot and CRC16 hash slot algorithm.

The stand-alone mode does not involve any partition idea, so we focus on the concept of partition mode and cluster mode.

2.1 slice mode

  • The fragmentation mode essentially belongs to client based fragmentation. The client implements the scheme of how to find the corresponding node in the Redis cluster according to a key.
  • Jedis's client fragmentation mode is implemented by consistency Hash. The advantage of consistency Hash algorithm is that when Redis nodes are added or deleted, it will only affect a small part of the data before and after adding or deleting nodes, which has a smaller impact on the data than modular algorithms.
  • Redis is used as a cache in most scenarios, so the impact of cache penetration caused by data loss does not need to be considered. When redis nodes are increased or decreased, the problem that some data cannot be hit can not be considered.

The overall application of sharding mode is shown in the figure below. The core lies in the consistency Hash strategy of the client.

(quoted from: www.cnblogs.com)

2.2 cluster mode

The cluster mode essentially belongs to the server fragmentation technology. The Redis cluster itself provides the fragmentation function, which has been officially provided since Redis version 3.0.

The principle of cluster is: a Redis cluster contains 16384 hash slots. Each key saved by Redis belongs to one of the 16384 hash slots. The cluster uses the formula CRC16(key)%16384 to calculate which slot the key belongs to. The CRC16(key) statement is used to calculate the CRC16 checksum of the key.

Each node in the cluster is responsible for processing a portion of the hash slot. For example, a cluster can have three hash slots, including:

  • Node A is responsible for processing hash slots 0 to 5500.
  • Node B is responsible for processing hash slots 5501 to 11000.
  • Node C is responsible for processing hash slots 11001 to 16383.

Redis reads and writes keys in cluster mode. First, it performs CRC16 calculation on the corresponding key value to obtain the corresponding hash value, takes the modulus of the hash value on the total number of slots and maps it to the corresponding slots, and finally maps it to the corresponding nodes for reading and writing. Take the command set("key", "value") as an example. It uses the CRC16 algorithm to calculate the key to obtain the hash value 28989, then takes the modulus of 16384 to obtain 12605, finally finds the redis node corresponding to 12605, and finally jumps to the node to execute the set command.

The overall application of cluster mode is shown in the figure below. The core lies in the design of cluster hash slot and redirection command.

(quoted from: www.jianshu.com)

3, Basic usage of Jedis

// Jedis stand alone mode access
public void main(String[] args) {
    // Create Jedis object
    jedis = new Jedis("localhost", 6379);
    // Perform the hmget operation
    jedis.hmget("foobar", "foo");
    // Close the Jedis object
    jedis.close();
}
 
// Access to Jedis sharding mode
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);
    // Create ShardedJedis object
    ShardedJedis shardedJedis = new ShardedJedis(shards);
    // Perform the set operation through the ShardedJedis object
    shardedJedis.set("a", "bar");
}
 
// Jedis cluster mode access
public void main(String[] args) {
    // Build redis cluster pool
    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));
 
    // Create JedisCluster
    JedisCluster cluster = new JedisCluster(nodes);
 
    // Execute the methods in the JedisCluster object
    cluster.set("cluster-test", "my jedis cluster test");
    String result = cluster.get("cluster-test");
}

Jedis realizes data access in stand-alone mode by creating jedis class objects, and realizes data access in cluster mode by building JedisCluster class objects.

To understand the whole process of Jedis accessing Redis, you can first understand the access process in stand-alone mode, and then analyze the access process in cluster mode.

4, Jedis stand alone mode access

The overall flow chart of Jedis accessing stand-alone Redis is shown below. It can be seen from the figure that the core process includes the creation of Jedis objects and the access to Redis through Jedis objects.

To be familiar with the process of Jedis accessing stand-alone Redis is to understand the creation process of Jedis and the process of executing Redis commands.

  • The core of Jedis creation process is to create Jedis object and Jedis internal variable Client object.
  • The process of Jedis accessing Redis is to access Redis through the Client object inside Jedis.

4.1 creation process

The class relationship diagram of Jedis itself is shown in the figure below. From the figure, we can see that Jedis inherits from BinaryJedis class.

In BinaryJedis class, there is a Client class object that interfaces with Redis. Jedis can read and write Redis through the Client object of BinaryJedis of the parent class.

During the creation of Jedis class, the Client object is created through the parent class BinaryJedis, and understanding the Client object is the key to further understanding the access process.

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) {
    // Create a parent BinaryJedis object
    super(host, port);
  }
}
 
public class BinaryJedis implements BasicCommands, BinaryJedisCommands, MultiKeyBinaryCommands,
    AdvancedBinaryJedisCommands, BinaryScriptingCommands, Closeable {
 
  // Accessing redis Client object
  protected Client client = null;
 
  public BinaryJedis(final String host, final int port) {
    // Create a Client object to access redis
    client = new Client(host, port);
  }
}

The class diagram of the Client class is shown in the following figure. The Client object inherits from BinaryClient and Connection classes. In BinaryClient class, there are Redis access password and other related parameters. In Connection class, there are socket objects and corresponding input / output streams to access Redis. In essence, Connection is the core class for communicating with Redis.

The Client class initializes the core parent class Connection object during creation, and the Connection is responsible for direct communication with Redis.

public class Client extends BinaryClient implements Commands {
  public Client(final String host, final int port) {
    super(host, port);
  }
}
 
public class BinaryClient extends Connection {
  // Storage and Redis connection related information
  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 {
  // Manage the socket information connected to Redis and the corresponding input / output stream
  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) {
    // Build DefaultJedisSocketFactory to create Socket objects connected to Redis
    this(new DefaultJedisSocketFactory(host, port, Protocol.DEFAULT_TIMEOUT,
        Protocol.DEFAULT_TIMEOUT, ssl, sslSocketFactory, sslParameters, hostnameVerifier));
  }
}

4.2 access process

Take Jedis executing the set command as an example. The whole process is as follows:

  • The set operation of Jedis is realized through the set operation of the Client.
  • The set operation of the Client is implemented through the sendCommand of the parent class Connection.
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 performs a set operation
    client.set(key, value);
    return client.getStatusCodeReply();
  }
}
 
public class Client extends BinaryClient implements Commands {
  @Override
  public void set(final String key, final String value) {
    // Execute the set command
    set(SafeEncoder.encode(key), SafeEncoder.encode(value));
  }
}
 
public class BinaryClient extends Connection {
  public void set(final byte[] key, final byte[] value) {
    // Send set instruction
    sendCommand(SET, key, value);
  }
}
 
public class Connection implements Closeable {
  public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    try {
      // socket connection redis
      connect();
      // Send commands according to redis protocol
      Protocol.sendCommand(outputStream, cmd, args);
    } catch (JedisConnectionException ex) {
    }
  }
}

5, Access to Jedis sharding mode

Based on the consistency Hash principle of Redis fragmentation mode introduced earlier, understand the access of Jedis fragmentation mode.

About the concept of redis fragmentation mode: redis did not have the concept of cluster mode before version 3.0, which leads to limited data that can be stored by a single node. Redis clients, such as Jedis, use consistency Hash algorithm to realize data fragmentation storage at the client.

In essence, Redis's fragmentation mode has nothing to do with Redis itself. It only solves the problem of limited storage of single node data through the client.

The core of ShardedJedis accessing Redis is to initialize the consistency Hash object when building the object, and build the mapping relationship between the classic Hash value of consistency Hash and node. After the mapping relationship is built, set and other operations are the addressing process from Hash value to node. After addressing, the single node operation is directly carried out.

5.1 creation process

The creation process of ShardedJedis is related to the initialization process of consistency Hash in Sharded of the parent class. The core is to build consistent virtual nodes and the mapping relationship between virtual nodes and Redis nodes.

The core part of the source code is to map 160 virtual nodes according to the weight, and locate the specific Redis node through the virtual node.

public class Sharded<R, S extends ShardInfo<R>> {
 
  public static final int DEFAULT_WEIGHT = 1;
  // Save the mapping relationship between the virtual node and the node node of redis
  private TreeMap<Long, S> nodes;
  // hash algorithm
  private final Hashing algo;
  // Save the connection information of the redis node and the Jedis accessing the node
  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<>();
    // Traverse each redis node and set the mapping relationship between the hash value and the node
    for (int i = 0; i != shards.size(); ++i) {
      final S shardInfo = shards.get(i);
      // It is mapped into 160 virtual nodes according to the weight
      int N =  160 * shardInfo.getWeight();
      if (shardInfo.getName() == null) for (int n = 0; n < N; n++) {
        // Build hash value and node mapping relationship
        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);
      }
      // Save access objects for each node
      resources.put(shardInfo, shardInfo.createResource());
    }
  }
}

5.2 access process

ShardedJedis's access process is the calculation process of consistency Hash. The core logic is: Hash the accessed key through the Hash algorithm to generate Hash value, obtain the corresponding Redis node according to the Hash value, and obtain the corresponding access object Jedis according to the corresponding Redis node.

After obtaining the access object Jedis, you can directly perform command operations.

public class Sharded<R, S extends ShardInfo<R>> {
 
  public static final int DEFAULT_WEIGHT = 1;
  private TreeMap<Long, S> nodes;
  private final Hashing algo;
  // Save the connection information of the redis node and the Jedis accessing the node
  private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<>();
 
  public R getShard(String key) {
    // Find the corresponding access object Jedis according to the redis node
    return resources.get(getShardInfo(key));
  }
 
  public S getShardInfo(String key) {
    return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
  }
 
  public S getShardInfo(byte[] key) {
    // Generate the corresponding hash value for the accessed key
    // Find the corresponding redis node according to the hash value
    SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
    if (tail.isEmpty()) {
      return nodes.get(nodes.firstKey());
    }
    return tail.get(tail.firstKey());
  }
}

6, Jedis cluster mode access

Understand the access of Jedis cluster mode based on the cluster principle of Redis introduced earlier.

The core mechanism by which Jedis can locate the key and hashing slot is the mapping of hash slot and Redis node, and this discovery process is based on Redis's cluster slot command.

Commands on Redis cluster operation: Redis will return the overall status of Redis cluster through cluster slots. The information returned for each Redis node includes:

  • Hash slot start number
  • Hash slot end number
  • The hash slot corresponds to the master node, which is represented by IP/Port
  • The first copy of the master node
  • The second copy of the master node
127.0.0.1:30001> cluster slots
1) 1) (integer) 0 // Start slot
   2) (integer) 5460 // End slot
   3) 1) "127.0.0.1" // host of master node
      2) (integer) 30001 // port of master node
      3) "09dbe9720cda62f7865eabc5fd8857c5d2678366" // Coding of nodes
   4) 1) "127.0.0.1" // host of slave node
      2) (integer) 30004 // port of slave node
      3) "821d8ca00d7ccf931ed3ffc7e3db0599d2271abf" // Coding of nodes
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"

The overall flow chart of Jedis accessing Redis in cluster mode is shown below. It can be seen from the figure that the core process includes the creation of JedisCluster object and the access to Redis through JedisCluster object.

The core of creating the JedisCluster object is to create the jedisclusterinfo cache object and establish the mapping relationship between the slot and the cluster node through cluster discovery.

JedisCluster accesses the Redis cluster by obtaining the Redis node where the key is located and accessing it through the Jedis object.

6.1 creation process

The class relationship of JedisCluster is shown in the figure below. You can see the core variable JedisSlotBasedConnectionHandler object in the figure.

BinaryJedisCluster, the parent class of JedisCluster, creates the JedisSlotBasedConnectionHandler object, which is responsible for communicating with the Redis cluster.

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) {
 
    // Accessing the parent class 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) {
 
    // Create the JedisSlotBasedConnectionHandler object
    this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
        connectionTimeout, soTimeout, user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMap);
 
    this.maxAttempts = maxAttempts;
  }
}

The core of jedisclotbasedconnectionhandler is to create and initialize the jedisclusterinfo cache object, which caches the information of Redis cluster.

The initialization process of jedisclusterinfo cache object is completed through initializesslotscache, which is mainly used to realize cluster node and slot discovery.

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) {
 
    // Create jedisclusterinfo cache object
    this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, infiniteSoTimeout,
        user, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
 
    // Initialize jedis Slot information
    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)) {
 
        // Cluster discovery through discoverClusterNodesAndSlots
        cache.discoverClusterNodesAndSlots(jedis);
        return;
      } catch (JedisConnectionException e) {
      }
    }
  }
}

nodes of jedisclusterinfo cache are used to save node information of Redis cluster, and slots are used to save slot and cluster node information.

The objects maintained by nodes and slots are JedisPool objects, which maintain the connection information with Redis. The cluster discovery process is implemented by discoverClusterNodesAndSlots. In essence, it is implemented by executing the Redis cluster discovery command cluster slots.

public class JedisClusterInfoCache {
  // It is responsible for saving the node information of the redis cluster
  private final Map<String, JedisPool> nodes = new HashMap<>();
  // It is responsible for saving the mapping relationship between the redis slot and the redis node
  private final Map<Integer, JedisPool> slots = new HashMap<>();
 
  // Responsible for cluster discovery logic
  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;
        }
        // Get the slot information corresponding to the redis node
        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);
          // Responsible for saving redis node information
          setupNodeIfNotExist(targetNode);
          if (i == MASTER_NODE_INDEX) {
            // It is responsible for saving the mapping relationship between the slot and the redis node
            assignSlotsToNode(slotNums, targetNode);
          }
        }
      }
    } finally {
      w.unlock();
    }
  }
 
  public void assignSlotsToNode(List<Integer> targetSlots, HostAndPort targetNode) {
    w.lock();
    try {
      JedisPool targetPool = setupNodeIfNotExist(targetNode);
      // Save the slot and the corresponding JedisPool object
      for (Integer slot : targetSlots) {
        slots.put(slot, targetPool);
      }
    } finally {
      w.unlock();
    }
  }
 
  public JedisPool setupNodeIfNotExist(HostAndPort node) {
    w.lock();
    try {
      // nodeKey corresponding to the production redis node
      String nodeKey = getNodeKey(node);
      JedisPool existingPool = nodes.get(nodeKey);
      if (existingPool != null) return existingPool;
      // JedisPool corresponding to the production redis node
      JedisPool nodePool = new JedisPool(poolConfig, node.getHost(), node.getPort(),
          connectionTimeout, soTimeout, infiniteSoTimeout, user, password, 0, clientName,
          ssl, sslSocketFactory, sslParameters, hostnameVerifier);
      // Save the key of the redis node and the corresponding JedisPool object
      nodes.put(nodeKey, nodePool);
      return nodePool;
    } finally {
      w.unlock();
    }
  }
}

The class relationship of JedisPool is shown in the following figure. The internal internalPool is pooled through apache common pool.

The internal pool inside JedisPool creates Jedis objects through the makeObject of JedisFactory.

Each Redis node corresponds to a JedisPool object. JedisPool is used to manage Jedis applications, release reuse, etc.

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 {
    // Create Jedis object
    final HostAndPort hp = this.hostAndPort.get();
    final Jedis jedis = new Jedis(hp.getHost(), hp.getPort(), connectionTimeout, soTimeout,
        infiniteSoTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
 
    try {
      // Jedis object connection
      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;
    }
    // Wrap the Jedis object as DefaultPooledObject for return
    return new DefaultPooledObject<>(jedis);
  }
}

6.2 access process

In the process of JedisCluster accessing Redis, the retry mechanism is realized through JedisClusterCommand, and finally the access is realized through Jedis object. From the perspective of implementation, JedisCluster encapsulates a layer on Jedis to locate cluster nodes and retry mechanisms.

Taking the set command as an example, the whole access is realized through JedisClusterCommand as follows:

  • Calculate the Redis node where the key is located.
  • Get the Jedis object corresponding to the Redis node.
  • set through Jedis object.
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);
  }
}

The core of the run method of JedisClusterCommand mainly locates the Redis node where the Redis key is located, and then obtains the Jedis object corresponding to the node for access.

After the Jedis object access exception, JedisClusterCommand will retry the operation and execute the renewSlotCache method according to certain policies to rediscover the cluster nodes.

public abstract class JedisClusterCommand<T> {
  public T run(String key) {
    // Calculate the slot position for 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 {
          // Get Jedis object according to slot
          connection = connectionHandler.getConnectionFromSlot(slot);
        }
      }
      // Execute the real Redis command
      return execute(connection);
    } catch (JedisNoReachableClusterNodeException jnrcne) {
      throw jnrcne;
    } catch (JedisConnectionException jce) {
 
      releaseConnection(connection);
      connection = null;
 
      if (attempts <= 1) {
        // Ensure the last two opportunities to refresh the corresponding information of the slot and node
        this.connectionHandler.renewSlotCache();
      }
      // Retry the operation according to the number of retries
      return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
    } catch (JedisRedirectionException jre) {
      // For the return of the Move command, the corresponding information of the slot and node is immediately triggered to be refreshed
      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);
    }
  }
}

The cache object of JedisSlotBasedConnectionHandler maintains the mapping relationship between slot and node, and obtains the Jedis object corresponding to the slot through getConnectionFromSlot method.

public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
 
  protected final JedisClusterInfoCache cache;
 
  @Override
  public Jedis getConnectionFromSlot(int slot) {
    // Get the JedisPool object corresponding to the slot
    JedisPool connectionPool = cache.getSlotPool(slot);
    if (connectionPool != null) {
      // Get Jedis object from JedisPool object
      return connectionPool.getResource();
    } else {
      // If the acquisition fails, refresh the slot information again
      renewSlotCache();
      connectionPool = cache.getSlotPool(slot);
      if (connectionPool != null) {
        return connectionPool.getResource();
      } else {
        //no choice, fallback to new connection to random node
        return getConnection();
      }
    }
  }
}

7, Pipeline implementation of Jedis

The core idea of Pipeline technology is to send multiple commands to the server without waiting for a reply. Finally, read the reply in one step. The advantage of this mode is that it saves the network overhead of request response mode.

The core difference between ordinary Redis commands such as set and Pipeline batch operations is that the set command will directly send a request to Redis and synchronously wait for the result to return, while the Pipeline operation will send a request but not immediately synchronously wait for the result to return. The specific implementation can be explored from the Jedis source code.

In the cluster mode, the related key s of the native Pipeline must be hashed to the same node to take effect. The reason is that the Client object under the Pipeline can only establish a connection to one of the nodes.

In the cluster mode, if keys belonging to different nodes can use Pipeline, the client object of the corresponding node needs to be saved for each key and obtained at the last time of data acquisition. In essence, it can be considered that a clustered Pipeline is encapsulated on the basis of a single node Pipeline.

7.1 Pipeline usage analysis

When a Pipeline accesses a single node Redis, it returns the Pipeline object through the Pipeline method of the Jedis object, and other command operations are accessed through the Pipeline object.

Pipeline is analyzed from the perspective of use. It will send multiple commands in batch, and finally use syncAndReturnAll to return results at one time.

public void pipeline() {
    jedis = new Jedis(hnp.getHost(), hnp.getPort(), 500);
    Pipeline p = jedis.pipelined();
    // Batch sending commands to redis
    p.set("foo", "bar");
    p.get("foo");
    // Synchronization waiting for response results
    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) {
    // dispatch orders
    getClient(key).set(key, value);
    // The getResponse of pipeline just aggregates the requests to be responded into the pipelinedResponses object
    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);
    // Uniformly save to the response queue
    pipelinedResponses.add(lr);
    return lr;
  }
}
 
 
public class Pipeline extends MultiKeyPipelineBase implements Closeable {
 
  public List<Object> syncAndReturnAll() {
    if (getPipelinedResponseLength() > 0) {
      // According to the number of commands sent in batch, that is, the number of commands to be returned in batch, read in batch through the client object
      List<Object> unformatted = client.getMany(getPipelinedResponseLength());
      List<Object> formatted = new ArrayList<>();
      for (Object o : unformatted) {
        try {
          // Format each returned result and finally save it in the list for return
          formatted.add(generateResponse(o).get());
        } catch (JedisDataException e) {
          formatted.add(e);
        }
      }
      return formatted;
    } else {
      return java.util.Collections.<Object> emptyList();
    }
  }
}

After sending a request to Redis, the normal set command immediately obtains the response result through getStatusCodeReply, so this is a request response mode.

When getStatusCodeReply obtains the response result, it will forcibly send a message to the Redis server through the flush() command, and then read the response result.

public class BinaryJedis implements BasicCommands, BinaryJedisCommands, MultiKeyBinaryCommands,
    AdvancedBinaryJedisCommands, BinaryScriptingCommands, Closeable {
 
  @Override
  public String set(final byte[] key, final byte[] value) {
    checkIsInMultiOrPipeline();
    // dispatch orders
    client.set(key, value);
    // Waiting for request response
    return client.getStatusCodeReply();
  }
}
 
 
public class Connection implements Closeable {
  public String getStatusCodeReply() {
    // Send request immediately via flush
    flush();
    // Processing response requests
    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 the output stream to ensure the sending of message
      outputStream.flush();
    } catch (IOException ex) {
      broken = true;
      throw new JedisConnectionException(ex);
    }
  }
}

8, Conclusion

As Redis's official preferred Java client development package, Jedis supports most Redis commands and is also a Redis client used more in daily life.

After understanding the implementation principle of Jedis, it can not only support the daily operation of Redis, but also better deal with the additional operations of Redis, such as technology selection during capacity expansion.

By introducing Jedis's three scene access modes for stand-alone mode, distribution mode and cluster mode, we can have a macro to micro understanding process, master Jedis's core ideas and better apply them to practice.

Author: vivo Internet server team - Wang Zhi

Posted by newbie8899 on Mon, 01 Nov 2021 18:52:47 -0700