Luga Lee
作者Luga Lee·2023-04-04 10:01
系统架构师·None

Redis 客户端 Jedis 的那点事

字数 12488阅读 1039评论 0赞 2

作为分布式缓存系统之一,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 实现的 R e dis 的 J ava 客户端 。以微服务体系为例,其主要应用于 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 redisTemplate(JedisConnectionFactory connectionFactory) {   
         RedisTemplate 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 对于应用程序的性能起着至关重要的作用 ,因此,我们应该深入地学习、熟练地掌握,并对其来龙去脉进行合理的把控显得格外重要。并结合实际的业务场景从而有效的提升系统整体运行效能。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

2

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广