深入剖析Redis客户端Jedis的特性和原理

【征服redis3】一文征服redis的jedis客户端 Jedis 是一个流行的 Java 编写的 Redis 客户端库,它提供了连接和操作 Redis 服务器的功能。简单易用:Jedis 提供了简洁的 API,方便开发人员使用,减少了连接和操作 Redis 的复杂性。高性能:Jedis 是通过直接与 Redis 服务器进行通信来实现操作的,因此具有较高的性能。支持多种数据类型:Jedis 支持操作 Redis 的各种数据类型,包括字符串、哈希、列表、集合、有序集合等。 阅读详情

一、开篇

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哈希槽算法来实现数据分布式存储。

单机模式不涉及任何分片的思想,所以我们着重分析分片模式和集群模式的理念。

2.1 分片模式

  • 分片模式本质属于基于客户端的分片,在客户端实现如何根据一个key找到Redis集群中对应的节点的方案。

  • Jedis的客户端分片模式采用一致性Hash来实现,一致性Hash算法的好处是当Redis节点进行增减时只会影响新增或删除节点前后的小部分数据,相对于取模等算法来说对数据的影响范围较小。

  • Redis在大部分场景下作为缓存进行使用,所以不用考虑数据丢失致使缓存穿透造成的影响,在Redis节点增减时可以不用考虑部分数据无法命中的问题。

分片模式的整体应用如下图所示,核心在于客户端的一致性Hash策略。

(引用自:www.cnblogs.com)

2.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命令。

集群模式的整体应用如下图所示,核心在于集群哈希槽的设计以及重定向命令。

(引用自:www.jianshu.com)

三、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。

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

4.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的寻址过程,寻址完成后直接进行单节点的操作。

5.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());
    }
  }
}

5.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对象进行访问。

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

6.2 访问过程

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。

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

八、结束语

Jedis作为Redis官方首选的Java客户端开发包,支持绝大部分的Redis的命令,也是日常中使用较多的Redis客户端。

了解了Jedis的实现原理,除了能够支持Redis的日常操作外,还能更好的应对Redis的额外操作诸如扩容时的技术选型。

通过介绍Jedis针对单机模式、分配模式、集群模式三种场景访问方式,让大家有个从宏观到微观的理解过程,掌握Jedis的核心思想并更好的应用到实践当中。

深入剖析Jedis客户端的特性和原理 Jedis客户端通过提供简单易用的API、支持丰富的数据类型、连接池管理、发布订阅模式和事务支持等特性,使得与Redis服务器的交互变得高效且便捷。Jedis是一个流行的Redis客户端,它提供了丰富的功能和易于使用的API,使得与Redis服务器进行交互变得简单而高效。事务支持:Redis支持事务操作,Jedis客户端通过提供事务相关的API,如multi()、exec()、discard()等,使得开发人员可以执行原子性的多个Redis命令。发布订阅模式:Jedis支持Redis的发布订阅模式。 阅读详情

相关推荐

Java Redis客户端Jedis-2.7.2实战指南

经过这一趟深度之旅,你应该已经明白:🔹Jedis本身不慢,慢的是使用方式不要滥用单连接,务必启用连接池。🔹连接池不是万能的,参数要调优maxTotal……每一项都影响稳定性。🔹数据结构选型决定性能天花板该用Hash就别用String,该用ZSet就别手动排序。🔹原子性需求交给Lua和WATCH复杂的业务逻辑,一定要在服务端原子完成。🔹批量操作必用Pipeline哪怕只是几十条,也能显著降低延迟。

weixin_30356433的博客 632

Redis

redis是一款开源的非关系型数据库(NoSQL),是由C语言去编写,基于Key-Value的NoSQL,而且Redis是基于内存存储数据的。官网链接:https://redis.io/ 3.2 测试 3.3 Jedis连接池的操作 四、Redis其他配置 4.1 Redis的AUTH 通过修改Redis的配置文件,实现Redis的密码校验 4.2 Redis的事务 没有回滚(所以需要配合 watch 一起使用) 4.3 Redis持久化机制 4.3.1 RDB 4.3.2 AOF 4.3.3

qq_48642210的博客 602

计算机模拟爆破过程,基于LSDYNA岩石爆破模拟建模分析

原标题:基于LSDYNA岩石爆破模拟建模分析 Ls Dyna岩石爆破模拟仿真作者:dyna_focus擅长领域:dyna/abaqus/hypermesh一、 数值模型的建立1.1 单元及算法的选择岩石,炸药,空气都采用solid164实体单元。岩石采用常应力实体单元,该种单元属于纯粹的lagrange算法,该种算法单元网格附着在材料上,随着材料的流动而产生单元网格的变形,但是在结构变形巨大时,有...

weixin_29494379的博客 7311

【redis】Java客户端Jedis

4.jedis连接池的使用方法。1.Java客户端Jedis。3.jedis使用方法。

weixin_43346403的博客 452

华为防火墙双ISP出口实战:如何用策略路由实现电信联通流量自动切换(附配置截图)

本文详细解析了华为防火墙在双ISP环境下的智能选路技术,通过策略路由与健康检测的深度应用,实现电信联通流量的自动切换。文章包含实战配置截图,帮助企业解决切换延迟、路径劣化等核心挑战,提升业务连续性。

backprop5master的博客 643

微信文件自动归档!手把手教你用OpenClaw搭个自己的“数字员工”,告别重复劳动

很多人说“AI是未来”,但未来不是靠喊口号的。OpenClaw的真正价值,是让你把重复劳动交出去,把时间留给真正需要创造力的事。这事真能干,而且不难。下载OpenClaw(GitHub仓库按我写的步骤跑一遍遇到坑,评论区留言——我亲自帮你看作者后记:上周五,我让AI自动归档了文件后,终于在5点准时下班。回家路上,我给它发了条消息:“干得漂亮”。它没回,但我知道——它在默默等我的下一条指令。

2601_94871597的博客 411

分布式缓存Redis之ShardedJedis

写在前面  本学习教程所有示例代码见GitHub:https://github.com/selfconzrr/Redis_Learning  原来项目中有用到Redis用作缓存服务,刚开始时只用一台Redis就能够满足服务,随着项目的慢慢进行,发现一台满足不了现有的项目需求,因为Redis操作都是原子性的,造成有时同时读写缓存导致查询效率的下降。但是由于我们现在用的还是Redis2.X版本,还是没有

ZhangRui的博客 4113

PyTorch深度学习实战(6)——神经网络性能优化技术

神经网络性能优化技术是指通过改进神经网络的结构、参数初始化、正则化和训练过程等方面来提高其性能和泛化能力的方法。优化技术的选择和调试需要根据具体问题进行,通过不断实验和调整,找到最适合网络的优化策略。本节首先训练了一个简单的全连接网络,然后在此基础上介绍了简单有效的神经网络性能提升技巧。

盼小辉丶的博客 1万+

连接池:别让连接池帮了倒忙

今天,我再与你说说另一种很重要的池化技术,即连接池。 我先和你说说连接池的结构。连接池一般对外提供获得连接、归还连接的接口给客户端使用,并暴露最小空闲连接数、最大连接数等可配置参数,在内部则实现连接建立、连接心跳保持、连接管理、空闲连接回收、连接可用性检测等功能。连接池的结构示意图,如下所示: 业务项目中经常会用到的连接池,主要是数据库连接池、Redis连接池和HTTP连接池。所以,今天我就以这三种连接池为例,和你聊聊使用和配置连接池容易出错的地方。 注意鉴别客户端SDK是否基于连接池 在使用三方客户端进

Java_Pluto的博客 704

FPGA+CUYSB3014实现USB3.0功能

CYUSB3014是赛普拉斯在近几年推出的新一代USB3.0的外设控制器,可以解决USB2.0带宽限制,或者单独开发USB协议和驱动的难题。赛普拉斯将CYUSB3014简称为EZ-USB FX3,具有高度的灵活特性,开发人员只需要下载FX3的固件库,就能使用USB3.0的功能。目前在一些电子产品中,使用主控器加PHY芯片最流行的方式是用FPGA+FX3这种搭配来实现USB3.0接口。......

D_Katter的博客 7065

【日志】Prometheus/Loki+Alertmanager+Grafana监控告警

参考文章:匹配写法:https://blog.ossq.cn/2674.html。

CN_Eden 1320

redis深入学习笔记

从底层到应用redis学习

like_java_的博客 2291

深入解析 Multi-Agent 协同决策机制与架构设计

让我们从一些基础定义开始,为后续的讨论建立共同的语言。在计算机科学和人工智能领域,智能体(Agent)是指一个能够感知环境、做出决策并执行动作的实体。智能体是任何可以通过传感器感知环境,并通过执行器作用于该环境的事物。atπstat​πst​sts_tst​是时刻ttt的环境状态(或智能体的观测)π\piπ是智能体的策略(Policy),即从状态到动作的映射ata_tat​是智能体在时刻ttt选择执行的动作多智能体系统(MAS)

AI天才研究院 539

全面剖析Redis Cluster原理和应用

全面剖析Redis Cluster原理和应用1.Redis Cluster总览1.1 设计原则和初衷在官方文档Cluster Spec中,作者详细介绍了Redis集群为什么要设计成现在的样子。最核心的目标有三个: 性能:这是Redis赖以生存的看家本领,增加集群功能后当然不能对性能产生太大影响,所以Redis采取了P2P而非Proxy方式、异步复制、客户端重定向等设计,而牺牲了部分的一致性、使用性。

西代零零发 10万+

智能车图像处理2-入门级分析1 ---基础扫线

PS :入门级文章适合新手,如果已经有基础可以去看进阶篇文章。

taiyuezyh的博客 7740

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Jedis深度解析:Java与Redis通信的桥梁

自2010年诞生以来,Jedis经历了多次重大更新,从最初的简单连接到支持集群、管道、事务等高级特性,已成为Java生态中最流行、最稳定的Redis客户端之一。本文将深入剖析Jedis的核心架构、关键特性、使用模式以及最佳实践。

一起探索IT的世界 1175

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

Java 连接并操作 Redis 万字详解:从 Jedis 直连到 RedisTemplate 封装,5 种方式全解析

本文系统讲解 Java 操作 Redis 的 5 种核心方式,从基础到进阶逐层解析。首先介绍 Jedis 直接连接与连接池技术,对比非池化与池化连接的原理及性能差异,演示连接池配置与资源复用机制。接着深入 Spring Data Redis 生态,详解 RedisTemplate 的高级抽象用法,包括 YAML 配置、数据类型操作及关键的 JSON 序列化配置,解决对象存储的序列化难题。针对字符串场景,对比 StringRedisTemplate 与 RedisTemplate 的区别。

Code哈哈笑的博客 2699

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

从明文到加密:Jedis客户端如何筑牢Redis数据传输安全防线

在当今数据驱动的时代,Redis作为高性能的内存数据库被广泛应用于各类系统中,而Jedis作为Java生态中最流行的Redis客户端之一,其数据传输的安全性直接关系到整个应用系统的安全基石。本文将深入剖析Jedis客户端如何通过SSL/TLS加密机制,为Redis数据传输构建一道坚实的安全防线,帮助开发者全面了解并正确配置Jedis的安全传输功能。 Redis作为一款高性能的键值存储数据库,在分

gitblog_01037的博客 779
上一篇: 面试官:什么是函数柯里化,能手写实现吗。
下一篇: 深入了解Mybatis架构设计
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值