Redis 客户端 Jedis 的那点事

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 作为分布式缓存系统之一,Redis 应用场景较为广泛,落地于不同的行业领域以及业务场景,因此,在整个架构拓扑中起着重要的作用。

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

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

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


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 操作等。具体如下所示:


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 1/2.x中,引入 Jedis 组件依赖,其在 pom.xml 文件中的配置如下:


<!-- 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 文件中定义相关参数,具体如下所示:


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 源码,具体如下所示:


@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 初始化的代码逻辑,可参考如下内容所示:


/**
*
* 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 部分源码已列出,具体可参考如下:


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 。具体可参考项目源码所示:


public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    setMinEvictableIdleTimeMillis(60000);
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
  }
}


    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 对于应用程序的性能起着至关重要的作用,因此,我们应该深入地学习、熟练地掌握,并对其来龙去脉进行合理的把控显得格外重要。并结合实际的业务场景从而有效的提升系统整体运行效能。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
NoSQL Redis 数据安全/隐私保护
Redis 最流行的图形化界面下载及使用超详细教程(带安装包)! redis windows客户端下载
文章提供了Redis最流行的图形化界面工具Another Redis Desktop Manager的下载及使用教程,包括如何下载、解压、连接Redis服务器以及使用控制台和查看数据类型详细信息。
257 6
Redis 最流行的图形化界面下载及使用超详细教程(带安装包)! redis windows客户端下载
|
3月前
|
NoSQL Redis 数据库
Redis 图形化界面下载及使用超详细教程(带安装包)! redis windows下客户端下载
文章提供了Redis图形化界面工具的下载及使用教程,包括如何连接本地Redis服务器、操作键值对、查看日志和使用命令行等功能。
233 0
Redis 图形化界面下载及使用超详细教程(带安装包)! redis windows下客户端下载
|
5月前
|
缓存 NoSQL Redis
【Azure Redis 缓存】Redission客户端连接Azure:客户端出现 Unable to send PING command over channel
【Azure Redis 缓存】Redission客户端连接Azure:客户端出现 Unable to send PING command over channel
261 3
|
3月前
|
NoSQL 网络协议 算法
Redis 客户端连接
10月更文挑战第21天
50 1
|
4月前
|
JSON NoSQL Java
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)
这篇文章介绍了在Java中使用Redis客户端的几种方法,包括Jedis、SpringDataRedis和SpringBoot整合Redis的操作。文章详细解释了Jedis的基本使用步骤,Jedis连接池的创建和使用,以及在SpringBoot项目中如何配置和使用RedisTemplate和StringRedisTemplate。此外,还探讨了RedisTemplate序列化的两种实践方案,包括默认的JDK序列化和自定义的JSON序列化,以及StringRedisTemplate的使用,它要求键和值都必须是String类型。
redis的java客户端的使用(Jedis、SpringDataRedis、SpringBoot整合redis、redisTemplate序列化及stringRedisTemplate序列化)
|
3月前
|
存储 消息中间件 NoSQL
Redis 入门 - C#.NET Core客户端库六种选择
Redis 入门 - C#.NET Core客户端库六种选择
80 8
|
4月前
|
NoSQL 网络协议 Java
[Redis] 渐进式遍历+使用jedis操作Redis+使用Spring操作Redis
[Redis] 渐进式遍历+使用jedis操作Redis+使用Spring操作Redis
59 7
|
4月前
|
NoSQL Java 网络安全
[Redis] 渐进式遍历+使用jedis操作Redis+使用Spring操作Redis
[Redis] 渐进式遍历+使用jedis操作Redis+使用Spring操作Redis
|
5月前
|
NoSQL Java Linux
Jedis测试redis。(redis在linux虚拟机中)
该博客文章提供了使用Jedis客户端连接Linux虚拟机中的Redis服务器的步骤,包括Maven依赖配置、测试用例编写以及测试结果的截图。
|
5月前
|
NoSQL 网络协议 Linux
【AKS+Redis】AKS中客户端(ioredis)遇见Azure Redis服务Failover后链接中断的可能性
【AKS+Redis】AKS中客户端(ioredis)遇见Azure Redis服务Failover后链接中断的可能性