lettuce连接池很香,撸撸它的源代码(下)

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: lettuce连接池很香,撸撸它的源代码

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 execution
        final 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();
    }

释放连接的流程图如下:

微信图片_20221212112546.png

看下关键代码

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类图如下:

微信图片_20221212112707.png

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个连接池

微信图片_20221212112803.png

上面关键代码就是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 版使用教程
云数据库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
相关文章
|
2月前
|
Java 关系型数据库 数据库连接
【SpringBoot】入门到精通 这一篇就够了
【SpringBoot】入门到精通 这一篇就够了
93 0
|
2月前
|
druid 网络协议 Java
再有人问你数据库连接池的原理,这篇文章甩给他!
在 Spring Boot 项目中,数据库连接池已经成为标配,然而,我曾经遇到过不少连接池异常导致业务错误的事故。很多经验丰富的工程师也可能不小心在这方面出现问题。 在这篇文章中,我们将探讨数据库连接池,深入解析其实现机制,以便更好地理解和规避潜在的风险。
|
2月前
|
缓存 NoSQL Java
手撸的 SpringBoot缓存系统,性能杠杠的
手撸的 SpringBoot缓存系统,性能杠杠的
35 0
SpringBoot进阶:4招优雅实现SpringBoot异步线程间数据传递
Spring Boot 自定义线程池实现异步开发相信看过陈某的文章都了解,但是在实际开发中需要在父子线程之间传递一些数据,比如用户信息,链路信息等等 比如用户登录信息使用ThreadLocal存放保证线程隔离,代码如下:
|
消息中间件 Java 微服务
还在用 OpenFeign?来试试 SpringBoot3 中的这个新玩意!
还在用 OpenFeign?来试试 SpringBoot3 中的这个新玩意!
1445 0
小白救星-SpringBoot最简教程04: lombok技术
当一个类的字段特别多时,整个文件看起来将非常不简洁。所幸IDEA提供了一款lombok插件,可以在代码阶段,通过添加注解的形式,省去手写getter, setter的麻烦。
|
消息中间件 缓存 安全
读懂HikariCP一百行代码,多线程就是个孙子!
读懂HikariCP一百行代码,多线程就是个孙子!
|
编解码 NoSQL 安全
lettuce连接池很香,撸撸它的源代码(上)
lettuce连接池很香,撸撸它的源代码
838 0
lettuce连接池很香,撸撸它的源代码(上)
|
存储 缓存 NoSQL
肝完了,总结了SpringBoot与缓存的知识点,快速掌握
本章给大家带来的是SpringBoot和缓存的学习。同时已经录制了非常详细的视频,如果看文档较为吃力,可以结合视频进行学习,帮你快速掌握SringBoot与缓存。
165 0
肝完了,总结了SpringBoot与缓存的知识点,快速掌握
|
NoSQL Java 数据库连接
SpringBoot入门,这一篇就够了(四)
SpringBoot入门,这一篇就够了
SpringBoot入门,这一篇就够了(四)