个人公众号:jinjunzhu

目录

springboot中lettuce配置

lettuce初始化

使用netty创建连接

管理连接

actuator健康检查获取连接

释放不掉的连接

共享连接

总结


Lettuce是一个高性能的redis客户端,底层基于netty框架来管理连接,天然是非阻塞和线程安全的。比起jedis需要为每个实例创建物理连接来保证线程安全,lettuce确实很优秀。本文主要介绍springboot使用lettuce整合redis客户端。说明一下,本文的源代码是使用springboot2.1.6,对应lettuce版本是5.1.7.RELEASE。

springboot中lettuce配置

springboot中配置lettuce是非常容易的,代码如下:

pom.xml文件

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.6.0</version>
</dependency>

application.properties配置

spring.redis.database=0
spring.redis.host=192.168.59.138
spring.redis.password=
spring.redis.port=6379
spring.redis.timeout=5000
#最大连接数
spring.redis.lettuce.pool.max-active=50
#最大阻塞等待时间
spring.redis.lettuce.pool.max-wait=5000
#连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=50
#连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=5
#eviction线程调度时间间隔
spring.redis.lettuce.pool.time-between-eviction-runs=1

redis配置类RedisConfig.java

@Configuration
public class RedisConfig {@BeanRedisTemplate redisTemplate(LettuceConnectionFactory factory){factory.setShareNativeConnection(false);RedisTemplate redisTemplate = new RedisTemplate();redisTemplate.setConnectionFactory(factory);return redisTemplate;}
}

上面3步就能完成springboot使用lettuce连接池整合redis的配置,之后我们就可以在业务类中注入RedisTemplate来使用了。

lettuce初始化

我们看一下整个初始化流程相关类的UML类图

LettuceConnectionConfiguration类是lettuce初始化的起始类,这个类是spring的管理的配置类,它初始化了lettuce连接工厂类,见如下代码

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public LettuceConnectionFactory redisConnectionFactory(ClientResources clientResources)throws UnknownHostException {LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources,this.properties.getLettuce().getPool());return createLettuceConnectionFactory(clientConfig);
}

初始化的过程会判断是单点模式/集群模式/哨兵模式,来初始化连接工厂,本文以单点模式为例来讲解

private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {if (getSentinelConfig() != null) {return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);}if (getClusterConfiguration() != null) {return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);}return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
}

获取到工厂类以后,lettuce会用如下2个Provider来获取和释放连接,分别管理普通模式和交互模式的连接。本示例采用单机的redis模式,所以初始化后的Provider是StandaloneConnectionProvider。

private @Nullable LettuceConnectionProvider connectionProvider;
private @Nullable LettuceConnectionProvider reactiveConnectionProvider;
public void afterPropertiesSet() {this.client = createClient();this.connectionProvider = createConnectionProvider(client, LettuceConnection.CODEC);this.reactiveConnectionProvider = createConnectionProvider(client, LettuceReactiveRedisConnection.CODEC);//省略部分代码}

注意:上面创建的provider类型是LettucePoolingConnectionProvider,它是StandaloneConnectionProvider的装饰器类,每次获取和释放连接,工厂类都会通过LettucePoolingConnectionProvider类调用LettucePoolingConnectionProvider的获取和释放操作

使用netty创建连接

lettuce的连接是靠netty来管理的,这或许是它性能优秀的重要原因。我们看一下通过netty来创建连接的代码,看一下StandaloneConnectionProvider的下面方法:

public <T extends StatefulConnection<?, ?>> T getConnection(Class<T> connectionType) {//省略部分代码if (StatefulConnection.class.isAssignableFrom(connectionType)) {return connectionType.cast(readFrom.map(it -> this.masterReplicaConnection(redisURISupplier.get(), it)).orElseGet(() -> client.connect(codec)));}throw new UnsupportedOperationException("Connection type " + connectionType + " not supported!");}

上面的client.connect(codec)是创建连接的代码,一直跟踪这个方法,

private void initializeChannelAsync0(ConnectionBuilder connectionBuilder, CompletableFuture<Channel> channelReadyFuture,SocketAddress redisAddress) {logger.debug("Connecting to Redis at {}", redisAddress);Bootstrap redisBootstrap = connectionBuilder.bootstrap();RedisChannelInitializer initializer = connectionBuilder.build();redisBootstrap.handler(initializer);clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);CompletableFuture<Boolean> initFuture = initializer.channelInitialized();ChannelFuture connectFuture = redisBootstrap.connect(redisAddress);//省略部分代码    }

管理连接

执行请求命令的时候首先要获取连接,流程图入下

关键代码

LettucePoolingConnectionProvider中getConnection

public <T extends StatefulConnection<?, ?>> T getConnection(Class<T> connectionType) {GenericObjectPool<StatefulConnection<?, ?>> pool = pools.computeIfAbsent(connectionType, poolType -> {return ConnectionPoolSupport.createGenericObjectPool(() -> connectionProvider.getConnection(connectionType),poolConfig, false);});try {StatefulConnection<?, ?> connection = pool.borrowObject();poolRef.put(connection, pool);return connectionType.cast(connection);} catch (Exception e) {throw new PoolException("Could not get a resource from the pool", e);}
}

GenericObjectPool中borrowObject

public T borrowObject(final long borrowMaxWaitMillis) throws Exception {//省略部分代码PooledObject<T> p = null;// Get local copy of current config so it is consistent for entire// method executionfinal boolean blockWhenExhausted = getBlockWhenExhausted();boolean create;final long waitTime = System.currentTimeMillis();while (p == null) {create = false;p = idleObjects.pollFirst();if (p == null) {p = create();if (p != null) {create = true;}}//省略部分代码}updateStatsBorrow(p, System.currentTimeMillis() - waitTime);return p.getObject();}

释放连接的流程图入下:

看下关键代码

GenericObjectPool中释放连接代码

public void returnObject(final T obj) {//省略部分代码final int maxIdleSave = getMaxIdle();if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {try {destroy(p);} catch (final Exception e) {swallowException(e);}} else {if (getLifo()) {idleObjects.addFirst(p);} else {idleObjects.addLast(p);}if (isClosed()) {// Pool closed while object was being added to idle objects.// Make sure the returned object is destroyed rather than left// in the idle object pool (which would effectively be a leak)clear();}}updateStatsReturn(activeTime);}

RedisChannalHandler中的close方法

public void close() {//省略部分代码closeAsync().join();
}
public CompletableFuture<Void> closeAsync() {//省略部分代码if (CLOSED.compareAndSet(this, ST_OPEN, ST_CLOSED)) {active = false;CompletableFuture<Void> future = channelWriter.closeAsync();//省略部分代码}return closeFuture;}

DefaultEndpoint类的closeAsync

public CompletableFuture<Void> closeAsync() {//省略部分代码if (STATUS.compareAndSet(this, ST_OPEN, ST_CLOSED)) {Channel channel = getOpenChannel();if (channel != null) {Futures.adapt(channel.close(), closeFuture);} else {closeFuture.complete(null);}}return closeFuture;}

actuator健康检查获取连接

我们知道,springboot的actuator健康检查是实现了ReactiveHealthIndicator接口,如果springboot工程启用了actuator,在lettuce初始化时,会创建一个reactive的连接,UML类图如下:

RedisReactiveHealthIndicator类会调用RedisConnectionFactory来创建一个reactive连接,代码如下:

protected Mono<Health> doHealthCheck(Health.Builder builder) {//getConnection()创建一个连接return getConnection().flatMap((connection) -> doHealthCheck(builder, connection));
}public LettuceReactiveRedisConnection getReactiveConnection() {//下面的构造函数会创建交互式连接return getShareNativeConnection()? new LettuceReactiveRedisConnection(getSharedReactiveConnection(), reactiveConnectionProvider): new LettuceReactiveRedisConnection(reactiveConnectionProvider);
}
LettuceReactiveRedisConnection(StatefulConnection<ByteBuffer, ByteBuffer> sharedConnection,LettuceConnectionProvider connectionProvider) {Assert.notNull(sharedConnection, "Shared StatefulConnection must not be null!");Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");//调用AsyncConnect构造函数创建连接方法this.dedicatedConnection = new AsyncConnect(connectionProvider, StatefulConnection.class);this.pubSubConnection = new AsyncConnect(connectionProvider, StatefulRedisPubSubConnection.class);this.sharedConnection = Mono.just(sharedConnection);}
AsyncConnect(LettuceConnectionProvider connectionProvider, Class<? extends T> connectionType) {Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");this.connectionProvider = connectionProvider;//回到了之前讲的使用connectionProvider创建连接Mono<T> defer = Mono.defer(() -> Mono.<T> just(connectionProvider.getConnection(connectionType)));this.connectionPublisher = defer.subscribeOn(Schedulers.elastic());}

释放不掉的连接

有时候我们为了节省创建连接花费的时间,会设置min-idle,但其实lettuce初始化时并不会创建这个数量的连接,除非我们设置一个参数spring.redis.lettuce.pool.time-between-eviction-runs=1,
而这样lettuce在初始化的时候因为使用了actuator做健康检查而创建${min-idle} + 1个reactive连接,并不会创建普通连接,只有在第一次请求的时候才会创建${min-idle} + 1个普通连接。
如果没有交互式场景,这些交互式连接不会被释放,造成资源浪费。所以如果使用了actuator监控检查,而又想初始化时创建一定数量的连接,只能造成连接资源浪费了。
为什么要这么设计,有点不明白,可能是bug?没顾上看后面的版本有没有处理这个问题。看下UML类图,从这个流程图看到,time-between-eviction-runs这个参数决定了是否初始化的时候创建${min-idle} + 1个连接池

上面关键代码就是GenericObjectPool类中的ensureMinIdle方法,在释放连接的时候也会调用这个方法,代码如下:

private void ensureIdle(final int idleCount, final boolean always) throws Exception {//省略部分代码while (idleObjects.size() < idleCount) {final PooledObject<T> p = create();if (p == null) {// Can't create objects, no reason to think another call to// create will work. Give up.break;}if (getLifo()) {idleObjects.addFirst(p);} else {idleObjects.addLast(p);}}if (isClosed()) {// Pool closed while object was being added to idle objects.// Make sure the returned object is destroyed rather than left// in the idle object pool (which would effectively be a leak)clear();}}

那为什么会比min-idle多创建一个连接呢?问题还在于上面的一个方法。初始化的流程如下:
1.健康检查需要创建一个reactive连接

protected Mono<Health> doHealthCheck(Health.Builder builder) {return getConnection().flatMap((connection) -> doHealthCheck(builder, connection));
}

2.之前介绍过,创建连接实际是用LettucePoolConnectionProvider的getConnection方法

public <T extends StatefulConnection<?, ?>> T getConnection(Class<T> connectionType) {GenericObjectPool<StatefulConnection<?, ?>> pool = pools.computeIfAbsent(connectionType, poolType -> {return ConnectionPoolSupport.createGenericObjectPool(() -> connectionProvider.getConnection(connectionType),poolConfig, false);});//省略部分代码
}

3.调用了ConnectionPoolSupport.createGenericObjectPool

public static <T extends StatefulConnection<?, ?>> GenericObjectPool<T> createGenericObjectPool(Supplier<T> connectionSupplier, GenericObjectPoolConfig config, boolean wrapConnections) {//省略部分代码GenericObjectPool<T> pool = new GenericObjectPool<T>(new RedisPooledObjectFactory<T>(connectionSupplier), config) {//省略部分代码};poolRef.set(new ObjectPoolWrapper<>(pool));return pool;
}

4.ConnectionPoolSupport.createGenericObjectPool方法创建GenericObjectPool对象,构造函数里面用到了前面讲的setConfig

public GenericObjectPool(final PooledObjectFactory<T> factory,final GenericObjectPoolConfig<T> config) {//省略部分代码setConfig(config);}

5.setConfig最终调用了上面讲的ensureIdle,而健康检查的那个连接还没有返还给线程池,线程池的数量已经是min-idle了,最终多了一个

同理,普通连接也是一样,首次创建的时候会比min-idle多一个

共享连接

第一部分介绍springboot整合lettuce时讲到RedisConfig的配置,如下方法里面第一行代码就是设置时是否共享Native连接。

@Bean
RedisTemplate redisTemplate(LettuceConnectionFactory factory){factory.setShareNativeConnection(false);RedisTemplate redisTemplate = new RedisTemplate();redisTemplate.setConnectionFactory(factory);return redisTemplate;
}

这个主要用于获取集群中的连接或者是获取Reactive连接时,可以用LettuceConnectionFactory中直接获取。我对这个地方的设计并不是特别理解,只是为了省去了从连接池获取和释放的的时间?

总结

lettuce的确很香,不过从设计中也可以看出一些瑕疵
如果应用使用了springboot的actuator,建议min-idle设置为0


本专栏精彩文章:

《redis灵魂拷问:为什么响应变慢了》

《redis灵魂拷问:聊一聊主从复制缓冲区》

《redis灵魂拷问:AOF文件可以保存RDB格式吗》

《redis灵魂拷问:聊一聊AOF日志重写》

《redis灵魂拷问:聊一聊redis底层数据结构》

《redis灵魂拷问:怎么搭建一个哨兵主从集群》


欢迎关注个人公众号

springboot研究九:lettuce连接池很香,撸撸它的源代码相关推荐

  1. springboot添加多数据源连接池并配置Mybatis

    springboot添加多数据源连接池并配置Mybatis 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/9190226.html May 12, 2018  ...

  2. springboot2整合redis使用lettuce连接池(解决lettuce连接池无效问题)

    lettuce客户端 Lettuce 和 Jedis 的都是连接Redis Server的客户端程序.Jedis在实现上是直连redis server,多线程环境下非线程安全(即多个线程对一个连接实例 ...

  3. spring-boot中使用druid连接池

      最近因为项目的要求,需要在spring-boot中配置druid连接池,数据库是Oracle,并且是多数据源的连接池,特地写下我的配置经历.   用的框架是spring-boot,数据库是orac ...

  4. Lettuce连接池超时问题简单记录

    使用Lettuce连接池,隔一段时间不连,再去连,就出现超时问题 问题分析定位 解决方案 方案一 方案二 问题分析定位 一直连着没问题,只要间隔一段时间不连,再去连的时候就会出现超时问题,超时几次后就 ...

  5. springboot集成lettuce连接池

    获取连接时流程图: 第一次访问时:RedisTemplate.java @Nullablepublic <T> T execute(RedisCallback<T> actio ...

  6. hikaridatasource连接池_细数springboot中的那些连接池

    hello~各位读者新年好! 回想起前几天在部署springboot项目到正线时,线上环境要求jdk7,可项目是基于jdk8开发的,springboot也是用的springboot2以上的版本,可以说 ...

  7. 【SpringBoot笔记】SpringBoot整合Druid数据连接池

    废话少说,按SpringBoot的老套路来. [step1]:添加依赖 <!-- 数据库连接池 --> <dependency><groupId>com.aliba ...

  8. hikaridatasource连接池_完美!细数SpringBoot中的那些“连接池”

    推荐阅读: 47天时间,洒热血复习,我成功"挤进"了字节跳动(附Java面试题+学习笔记+算法刷题)​zhuanlan.zhihu.com 拼多多终面竟被"Out&quo ...

  9. Lettuce连接池

    Lettuce 连接被设计为线程安全,所以一个连接可以被多个线程共享,同时lettuce连接默认是自动重连.虽然连接池在大多数情况下是不必要的,但在某些用例中可能是有用的.lettuce提供通用的连接 ...

最新文章

  1. Android 网络操作常用的两个类
  2. (一)TestNG测试框架之HelloWorld入门
  3. 提升网站优化排名的前提是什么?
  4. springMVC项目国际化(i18n)实现方法
  5. android中播放gif动画之二
  6. 叠加卡片列表_使用PowerBI制作卡片图
  7. kvm+libvirt虚拟机快照浅析[转]
  8. 对比几个空地址“订阅”的结果
  9. html遮罩水印,CSS滤镜遮罩,图片水印效果
  10. 跨境电商指南:如何处理客户投诉
  11. 笔记本外接2-3个屏幕
  12. require与import的区别和使用(CommonJS规范和es6规范)
  13. 韩国的开发者开源项目 paper with code论文自动下载
  14. 苹果cms10自适应模板好看的苹果cmsv10美化模板免费
  15. Linux 5.19 迎来首个候选版本,Linus:多平台 Linux 内核“差不多完成了”
  16. 网络请求未知错误 CLEARTEXT communication to XX not permitted by network security policy 解决方案
  17. 定义符号常量pi.const float pi= 3.1415926f; 这句话是否正确
  18. 访客登记使用人脸识别有什么好处?
  19. Redux、React-Redux入门笔记
  20. 让企业用上金融合规专有云,腾讯云专有云TCE矩阵来支招儿!

热门文章

  1. 计算机维修店起名字,家电维修店名字大全
  2. 线性判别分析(LDA)原理详解
  3. c语言如何实现软件抗干扰,单片机软件系统抗干扰设计
  4. 51单片机实时时钟显示
  5. FANUC机器人SYST-212报警PAUSE.G需要应用DCS参数的解决办法
  6. 值得关注的5个在线HTML5工具
  7. Python入门要多长时间?要学什么?
  8. 【无标题】长期招聘硬件工程师-深圳宝安
  9. Java类似相机中图像处理(上)
  10. python 可变数据类型和不可变数据类型