前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis 客户端 Jedis 的那点事

Redis 客户端 Jedis 的那点事

原创
作者头像
Luga Lee
修改2021-11-22 10:22:24
1.8K0
修改2021-11-22 10:22:24
举报
文章被收录于专栏:架构驿站

作为分布式缓存系统之一,Redis 应用场景较为广泛,落地于不同的行业领域以及业务场景,因此,在整个架构拓扑中起着重要的作用。

     Redis ,全称为 “Remote Dictionary Server ”,即:远程字典服务器。一款完全开源免费,基于 C 语言编写,遵守 BSD 协议,高性能的 ( Key/Value ) 分布式内存数据库。其基于内存运行并支持持久化的 NoSQL 数据库, 是当前最热门的 NoSQL 数据库之一,通常也被称之为“数据结构服务器”。Redis 为典型的 C/S 架构,基于 Java 语言平台,其使用 Socket、Redis 的 RESP(Redis Serialization Protocol 即 Redis 序列化协议)协议进行业务处理。作为一款备受欢迎的组件,其主要应用于如下场景中:缓存、计数器、购物车、点赞/打卡、分布式锁等等。

     事件背景:在某一次的业务量高峰时刻,应用后台服务抛出“读超时”异常,具体如下所示:

代码语言:javascript
复制
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out

     基于此,我们正式进入本文正题,以探讨 Redis 客户端 Jedis 的相关技术,深入挖掘其底层技术,使得大家能够对整个 Redis 技术体系有所了解。

     截至目前,在实际的业务场景中,Redis 客户端主要有以下 3 种,具体如下所示:

     1、Jedis ,作为一款老牌、流行的 Redis 的 Java 实现客户端,其提供了比较全面的 Redis 命令的支持。其基于阻塞 I/O,且其方法调用为同步,程序流需要等到 Sockets 处理完 I/O 才能执行,不支持异步。Jedis 客户端实例不是线程安全的,所以需要通过连接池来使用 Jedis 。

     2、Lettuce ,一款高级的 Redis 客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器等。其基于 Netty 框架的事件驱动的通信层,其方法调用为异步。Lettuce 的 API 是线程安全的,所以可以操作单个 Lettuce 连接来完成各种操作。Lettuce 需要 Java 8 及以上版本运行平台,其能够支持 Redis Version 4 以实现与 Redis 服务端进行同步和异步的通信。

     3、Redisson ,一款基于实现分布式和可扩展的 Java 数据结构,促使开发人员对 Redis 的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过 Redis 支持延迟队列。其基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。Redisson 的 API 是线程安全的,所以可以操作单个 Redisson 连接来完成各种操作。

     接下来,我们重点来了解下 Jedis 组件。

     Jedis 是一款基于 BIO 实现的 Redis 的 Java 客户端。以微服务体系为例,其主要应用于 Spring Boot 1.x 中,在 Spring Boot 2.0 后,其默认已被 Lettuce 所取代。当然,在 Spring Boot 2.x 中,Jedis 也可以继续使用,依据 Jedis 的相关配置规范。Jedis 包含以下几个核心类与服务端交互:Jedis、JedisCluster、ShardedJedis、JedisPool、JedisSentinelPool 以及 ShardedJedisPool。我们先来了解下 Jedis 的 UML 图,具体如下:

图片
图片

     通过源码(项目地址:https://github.com/redis/jedis )可以看到:

    Jedis 继承了 BinaryJedis 同时实现了一系列的 Commands 接口,BinaryJedis 里主要和 Redis Server 进行交互,一系列 Commands 接口主要是对 Redis 支持的接口进行分类,像 BasicCommands 主要包含了 Info、Flush 等操作,BinaryJedisCommands 主要包含了 Get、Set 等操作,MultiKeyBinaryCommands 主要包含了一些批量操作的接口,例如 Mset 操作等。具体如下所示:

代码语言:javascript
复制
public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands,
    AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands, ModuleCommands {
    ...
}

     基于源码所述,我们可以看到:Jedis 对象的继承关系:Jedis—>BinaryJedis- ->BasicCommands、BinaryJedisCommands等,其中 BinaryJedis 组合了 Client 对象 (Client—>BinaryClient—>Connection,Connection 对象组合了 Socket、输入输出流等连接对象)。

    我们来看一下 Jedis 对应 Redis 的四种工作模型,Redis Standalone(单节点模式)、Redis Cluster(集群模式)、Redis Sentinel(哨兵模式)以及 Redis Sharding(分片模式),具体如下示意图所示:

图片
图片

     Jedis 实例通过 Socket 建立客户端与服务端的长连接,往 OutputStream 发送命令,从 InputStream 读取回复。其主要包括 3 种调用模式,具体如下:

     1、Client 模式

     Client 模式就是常用的 “所见即所得”,客户端发一个命令,阻塞等待服务端执行,然后读取返回结果。优点是确保每次处理都有结果,一旦发现返回结果中有 Error,就可以立即处理。

     2、Pipeline 模式

     Pipeline 模式则是一次性发送多个命令,最后一次取回所有的返回结果,这种模式通过减少网络的往返时间和 IO 的读写次数,大幅度提高通信性能,但 Pipeline 不支持原子性,如果想保证原子性,可同时开启事务模式。

     3、Transaction 模式

    Transaction 模式即开启 Redis 的事务管理,Pipeline 可以在事务中,也可以不在事务中。事务模式开启后,所有的命令(除了 EXEC 、 DISCARD 、 MULTI 和 WATCH )到达服务端以后,不会立即执行,会进入一个等待队列,等到收到下述四个命令时执行不同操作。

     在 Spring Boot v1/2.x 中,引入 Jedis 组件依赖,其在 pom.xml 文件中的配置如下:

代码语言:javascript
复制
<!-- Redis -->
<!--若基于 Spring Boot 2.0及以上版本,则Redis默认使用的Lettuce客户端-->  
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
      <exclusions>
        <!-- 排除lettuce包 -->
        <exclusion>
          <groupId>io.lettuce</groupId>
          <artifactId>lettuce-core</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- 添加jedis客户端 -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.1.0</version>
    </dependency>
 <!--使用默认的Lettuce时,若配置spring.redis.lettuce.pool则必须配置该依赖-->
 <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
 </dependency>

     注:需要注意的是,若指定了 Jedis Pool 属性,那么需要在 pom.xml 文件中加入 commons-pool2 的依赖。

     然后,在对应的 yaml 文件中定义相关参数,具体如下所示:

代码语言:javascript
复制
spring:
  redis:
    cluster:
      nodes: 10.10.10.1:6380,10.10.10.2:6380,10.10.10.3:6380,10.10.10.4:6380,10.10.10.5:6380,10.10.10.6:6380
      max-redirects: 2
    jedis:
      pool:
        max-active: 20 # 连接池最大连接数(使用负值表示没有限制),默认为8      
        max-idle: 20 # 连接池中的最大空闲连接,默认为8 
        min-idle: 20 # 连接池中的最小空闲连接,默认为0
        max-wait: 100 # 连接池最大阻塞等待时间(使用负值表示没有限制)
    timeout: 300 # 连接超时时间(毫秒)

     接下来,我们在看一下 JedisRedisConfig 源码,具体如下所示:

代码语言:javascript
复制
@Configuration
public class JedisRedisConfig {

  @Value("${spring.redis.database}")
  private int database;
  @Value("${spring.redis.host}")
  private String host;
  @Value("${spring.redis.port}")
  private int port;
  @Value("${spring.redis.password}")
  private String password;
  @Value("${spring.redis.timeout}")
  private int timeout;
  @Value("${spring.redis.jedis.pool.max-active}")
  private int maxActive;
  @Value("${spring.redis.jedis.pool.max-wait}")
  private long maxWaitMillis;
  @Value("${spring.redis.jedis.pool.max-idle}")
  private int maxIdle;
  @Value("${spring.redis.jedis.pool.min-idle}")
  private int minIdle;

  /**
   * 连接池配置信息
   */

  @Bean
  public JedisPoolConfig jedisPoolConfig() {
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    // 最大连接数
    jedisPoolConfig.setMaxTotal(maxActive);
    // 当池内没有可用连接时,最大等待时间
    jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
    // 最大空闲连接数
    jedisPoolConfig.setMinIdle(maxIdle);
    // 最小空闲连接数
    jedisPoolConfig.setMinIdle(minIdle);
    // 其他属性可以自行添加
    return jedisPoolConfig;
  }

  /**
   * Jedis 连接
   * 
   * @param jedisPoolConfig
   * @return
   */
  @Bean
  public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig) {
    JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder().usePooling()
        .poolConfig(jedisPoolConfig).and().readTimeout(Duration.ofMillis(timeout)).build();
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
    redisStandaloneConfiguration.setHostName(host);
    redisStandaloneConfiguration.setPort(port);
    redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
    return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration);
  }

  /**
   * 缓存管理器
   * 
   * @param connectionFactory
   * @return
   */
  @Bean
  public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.create(connectionFactory);
  }

  @Bean
  public RedisTemplate<String, Serializable> redisTemplate(JedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    redisTemplate.setConnectionFactory(jedisConnectionFactory(jedisPoolConfig()));
    return redisTemplate;
  }
  
}

     接下来,我们再来了解一下 Jedis 的基本工作原理,具体可参考如下活动图所示:

图片
图片

     基于上述参考示意图,Jedis 通过传入 Redis Server 地址信息(host,port)进行初始化相关工作,然后在 BinaryJedis 里实例化 Client。Client 通过 Socket 维持客户端与 Redis 服务器的连接与沟通。至于上文所提到 Transaction 和 Pipeline ,其原理几乎很相似,它们继承同一个基类 MultiKeyPipelineBase。区别在于 Transaction 在实例化时,就自动发送 MULTI 命令,开启事务模式,而 Pipeline 则需依据实际情况进行手动开启,两种模式均需要依靠 Client 发送命令。关于 Transaction 和 Pipeline 初始化的代码逻辑,可参考如下内容所示:

代码语言:javascript
复制
/** 
*
* BinaryJedis类 
*
*/
public Transaction multi() {
    client.multi();
    transaction = new Transaction(client);
    return transaction;
}
public Pipeline pipelined() {
    pipeline = new Pipeline();
    pipeline.setClient(client);
    return pipeline;
}

     在实际的业务场景中,我们大多数场景下都是基于 Jedis Pool 进行业务逻辑操作,毕竟,直接使用 Jedis 不能避免的需要反复的进行 Socket 的创建和销毁,对于资源角度而言,其开销较为庞大。JedisPool 的构造方法很多,通常情况下,一般默认可以通过 JedisConfig 进行配置,在前面的 JedisRedisConfig 部分源码已列出,具体可参考如下:

代码语言:javascript
复制
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxActive(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWait(MAX_WAIT);
config.setMaxWait(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);

     在 Jedis Pool 中,其继承关系可简要梳理为:JedisPool —> JedisPoolAbstract —> Pool 。通常而言,JedisPool 使用了 Apache Commons-pool2 框架,该框架提供了池化方案,可以在本地维护一个对象池,作为使用者我们只需要提供创建对象等一些简单的操作即可,接入较为简单。综上,可以概括为:Jedis 的对象池的资源管理内部是使用 Apache Commons-pool2 (后边将其简称为“ ACP ”)开源工具包来实现的。那么,此对象池是如何管理呢?

     通常来讲,ACP 是一个通用的资源池管理框架,内部会定义好资源池的接口和规范,具体创建对象实现交由具体框架来实现。具体如下:

     1、从资源池获取对象,会调用 ObjectPool#borrowObject,如果没有空闲对象,则调用 PooledObjectFactory#makeObject 创建对象,JedisFactory 是具体的实现类。

    2、创建完对象放到资源池中,返回给客户端使用。

    3、使用完对象会调用 ObjectPool#returnObject,其内部会校验一些条件是否满足,验证通过,对象归还给资源池。

    4、条件验证不通过,比如资源池已关闭、对象状态不正确(Jedis连接失效)、已超出最大空闲资源数,则会调用 PooledObjectFactory#destoryObject 从资源池中销毁对象。

     具体的调用关系,我们先来了解下如下所示:

调用关系图
调用关系图

     基于上述关系可知:ObjectPool 和 KeyedObjectPool 是两个基础接口。ObjectPool 接口资源池列表里存储都是对象,默认实现类 GenericObjectPool。KeyedObjectPool 接口用键值对的方式维护对象,默认实现类是 GenericKeyedObjectPool。在实现过程会有很多公共的功能实现,放在了 BaseGenericObjectPool 基础实现类当中。

     SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个 SoftReference 中。

     SoftReference 软引用,能够在 JVM GC 过程中当内存不足时,允许垃圾回收机制在需要释放内存时回收对象池中的对象,避免内存泄露的问题。

     PooledObject 是池化对象的接口定义,池化的对象都会封装在这里。

     DefaultPooledObject 是 PooledObject 接口缺省实现类,PooledSoftReference 使用 SoftReference 封装了对象,供 SoftReferenceObjectPool 使用。具体引用关系可参考如下:

调用关系图
调用关系图

     接下来,我们再了解下 Jedis 客户端参数相关内容,Jedis 客户端资源池参数都是基于 JedisPoolConfig 构建的。JedisPoolConfig 继承了 GenericObjectPoolConfig 。具体可参考项目源码所示:

代码语言:javascript
复制
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxActive(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWait(MAX_WAIT);
config.setMaxWait(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);

     JedisPoolConfig 默认构造器中会对代码中的以下相关参数进行默认的初始化操作,具体如下:

     testWhileIdle 参数设置为 true(默认为 false)

     minEvictableIdleTimeMillis 设置为 60 秒(默认为 30 分钟)

     timeBetweenEvictionRunsMillis 设置为 30 秒(默认为 -1)

     numTestsPerEvictionRun 设置为 -1(默认为 3)

     即:每隔 30 秒执行一次空闲资源监测,发现空闲资源超过 60 秒未被使用,从资源池中移除。

     基于源码所述,我们可以梳理出:GenericObjectPoolConfig 里的参数可大致将其归类为以下三组:

     1、核心参数,具体主要包括以下:

     maxTotal:资源池中的最大连接数,默认为 8

     maxIdle:资源池允许的最大空闲连接数,默认为 8

     minIdle:资源池确保的最少空闲连接数,默认为 0

     2、空闲资源检测相关参数,主要包含以下:

     testWhileIdle:是否开启空闲资源检测,默认 false

     timeBetweenEvictionRunsMillis:空闲资源的检测周期(单位为毫秒),默认 600000 即 10 分钟

     minEvictableIdleTimeMillis:资源池中资源的最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除,默认 1800000 即 30 分钟

     softMinEvictableIdleTimeMillis:资源池中资源的最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除,默认 1800000 即 30 分钟,与 minEvictableIdleTimeMillis 的区别见后边的源码解析

     numTestsPerEvictionRun:做空闲资源检测时,每次检测资源的个数,默认为 3

     3、其他辅助参数,具体涉及以下:

    blockWhenExhausted:当资源池用尽后,调用者是否要等待。只有当值为 true 时,下面的 maxWaitMillis 才会生效。默认为 true

     maxWaitMillis:当资源池连接用尽后,调用者的最大等待时间(单位为毫秒),默认为 -1 表示永不超时

     testOnBorrow:向资源池借用连接时是否做连接有效性检测(ping),检测到的无效连接将会被移除,默认 fase

     testOnReturn:向资源池归还连接时是否做连接有效性检测(ping),检测到无效连接将会被移除,默认 fase

     jmxEnabled:是否开启 JMX 监控,默认为 ture

     其实,在实际的业务场景中,摒弃系统指定的默认参数,最为关键的当属“核心参数”,以下为阿里云官方给出的相关优化建议(当然,仅供参考,具体以实际的业务场景调优为准):

     1、maxTotal(最大连接数)

     想合理设置 maxTotal(最大连接数)需要考虑的因素较多,如:

(1)业务希望的 Redis 并发量

(2)客户端执行命令时间

   (3)Redis 资源,例如 Nodes(如应用 ECS 或 VM 个数等) * maxTotal 不能超过 Redis 的最大连接数

   (4)资源开销,例如虽然希望控制空闲连接,但又不希望因为连接池中频繁地释放和创建连接造成不必要的开销

     场景:假设一次命令时间,即 borrow|return resource 加上 Jedis 执行命令 ( 含网络耗时)的平均耗时约为 1ms,一个连接的 QPS 大约是 1s/1ms = 1000,而业务期望的单个 Redis 的 QPS 是 50000(业务总的 QPS/Redis 分片个数),那么理论上需要的资源池大小(即 MaxTotal)是 50000 / 1000 = 50。

     但事实上这只是个理论值,除此之外还要预留一些资源,所以 maxTotal 可以比理论值大一些。这个值不是越大越好,一方面连接太多会占用客户端和服务端资源,另一方面对于 Redis 这种高 QPS 的服务器,如果出现大命令的阻塞,即使设置再大的资源池也无济于事。

     2、maxIdle 与 minIdle

     maxIdle 实际上才是业务需要的最大连接数,maxTotal 是为了给出余量,所以 maxIdle 不要设置得过小,否则会有 new Jedis(新连接)开销,而 minIdle 是为了控制空闲资源检测。

     连接池的最佳性能是 maxTotal=maxIdle,这样就避免了连接池伸缩带来的性能干扰。如果您的业务存在突峰访问,建议设置这两个参数的值相等;如果并发量不大或者 maxIdle 设置过高,则会导致不必要的连接资源浪费。

     从这个建议我们可以得出如下结论:在默认的场景下 maxTotal 和 maxIdle 应设置为相同的值,结合相关数据也可以计算出对应的值,虽然 minIdle 参数没有明确说明,但我们可以结合源码(此处因篇幅原因暂无列出)描述以及基于实际的业务场景尝试进行不断的优化,以寻求最优性能。

    作为 Redis 的 Java 客户端 Jedis 的底层管理机制,Apache Commons-pool2 对于应用程序的性能起着至关重要的作用,因此,我们应该深入地学习、熟练地掌握,并对其来龙去脉进行合理的把控显得格外重要。并结合实际的业务场景从而有效的提升系统整体运行效能。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档