关于java:SpringBoot默认的连接池-HikariCP

77次阅读

共计 23316 个字符,预计需要花费 59 分钟才能阅读完成。

HikariCP

当初曾经有很多公司在应用 HikariCP 了,HikariCP 还成为了 SpringBoot 默认的连接池,随同着 SpringBoot 和微服务,HikariCP 必将迎来宽泛的遍及。

上面陈某带大家从源码角度剖析一下 HikariCP 为什么可能被 Spring Boot 青眼,文章目录如下:

目录

零、类图和流程图

开始前先来理解下 HikariCP 获取一个连贯时类间的交互流程,不便上面具体流程的浏览。

获取连贯时的类间交互:

图 1

一、主流程 1:获取连贯流程

HikariCP 获取连贯时的入口是 HikariDataSource 里的 getConnection 办法,当初来看下该办法的具体流程:

主流程 1

上述为 HikariCP 获取连贯时的流程图,由图 1 可知,每个 datasource 对象里都会持有一个 HikariPool 对象,记为 pool,初始化后的 datasource 对象 pool 是空的,所以第一次 getConnection 的时候会进行 实例化 pool属性(参考 主流程 1 ),初始化的时候须要将以后 datasource 里的 config 属性 传过来,用于 pool 的初始化,最终标记 sealed,而后依据 pool 对象调用getConnection 办法(参考 流程 1.1),获取胜利后返回连贯对象。

二、主流程 2:初始化池对象

主流程 2

该流程用于 初始化整个连接池,这个流程会给连接池内所有的属性做初始化的工作,其中比拟次要的几个流程上图曾经指出,简略概括一下:

  1. 利用 config 初始化各种连接池属性,并且产生一个用于 生产物理连贯 的数据源DriverDataSource
  2. 初始化寄存连贯对象的外围类connectionBag
  3. 初始化一个延时工作线程池类型的对象 houseKeepingExecutorService,用于后续执行一些延时 / 定时类工作(比方连贯透露查看延时工作,参考 流程 2.2以及 主流程 4 ,除此之外 maxLifeTime 后被动回收敞开连贯也是交由该对象来执行的,这个过程能够参考 主流程 3
  4. 预热连接池,HikariCP 会在该流程的 checkFailFast 里初始化好一个连贯对象放进池子内,当然触发该流程得保障 initializationTimeout > 0 时(默认值 1),这个配置属性示意留给预热操作的工夫(默认值 1 在预热失败时不会产生重试)。与 Druid 通过 initialSize 管制预热连贯对象数不一样的是,HikariCP 仅预热进池一个连贯对象。
  5. 初始化一个线程池对象addConnectionExecutor,用于后续裁减连贯对象
  6. 初始化一个线程池对象 closeConnectionExecutor,用于敞开一些连贯对象,怎么触发敞开工作呢?能够参考 流程 1.1.2

三、流程 1.1:通过 HikariPool 获取连贯对象

流程 1.1

从最开始的结构图可知,每个 HikariPool 里都保护一个 ConcurrentBag 对象,用于寄存连贯对象,由上图能够看到,实际上 HikariPoolgetConnection就是从 ConcurrentBag 里获取连贯的(调用其 borrow 办法取得,对应 ConnectionBag 主流程),在长连贯查看这块,与之前说的Druid 不同,这里的长连贯判活查看在连贯对象没有被标记为“已抛弃 ”时,只有间隔上次应用超过500ms 每次取出都会进行查看(500ms 是默认值,可通过配置 com.zaxxer.hikari.aliveBypassWindowMs 的零碎参数来管制),emmmm,也就是说 HikariCP 对长连贯的活性查看很频繁,然而其并发性能仍旧优于Druid,阐明频繁的长连贯查看并不是导致连接池性能高下的关键所在。

这个其实是因为 HikariCP 的 无锁 实现,在高并发时对 CPU 的负载没有其余连接池那么高而产生的并发性能差别,前面会说 HikariCP 的具体做法,即便是 Druid,在 获取连贯 生成连贯 偿还连贯 时都进行了 锁管制 ,因为通过上篇解析Druid 的文章能够晓得,Druid里的连接池资源是多线程共享的,不可避免的会有锁竞争,有锁竞争意味着线程状态的变动会很频繁,线程状态变动频繁意味着 CPU 上下文切换也将会很频繁。

回到 流程 1.1,如果拿到的连贯为空,间接报错,不为空则进行相应的查看,如果查看通过,则包装成 ConnectionProxy 对象返回给业务方,不通过则调用 closeConnection 办法敞开连贯(对应 流程 1.1.2,该流程会触发 ConcurrentBagremove办法抛弃该连贯,而后把理论的驱动连贯交给 closeConnectionExecutor 线程池,异步敞开驱动连贯)。

四、流程 1.1.1:连贯判活

流程 1.1.1

承接下面的 流程 1.1里的判活流程,来看下判活是如何做的,首先说验证办法(留神这里该办法承受的这个 connection 对象不是 poolEntry,而是poolEntry 持有的理论驱动的连贯对象),在之前介绍 Druid 的时候就晓得,Druid 是依据驱动程序里是否存在 ping 办法 来判断是否启用 ping 的形式判断连贯是否存活,然而到了 HikariCP 则更加简略粗犷,仅依据是否配置了 connectionTestQuery 觉定是否启用 ping:

this.isUseJdbc4Validation = config.getConnectionTestQuery() == null;

所以个别驱动如果不是特地低的版本,不倡议配置该项,否则便会走 createStatement+excute 的形式,相比 ping 简略发送心跳数据,这种形式显然更低效。

此外,这里在刚进来还会通过驱动的连贯对象从新给它设置一遍 networkTimeout 的值,使之变成 validationTimeout,示意一次验证的超时工夫,为啥这里要从新设置这个属性呢?因为在应用 ping 办法校验时,是没方法通过相似statement 那样能够 setQueryTimeout 的,所以只能由网络通信的超时工夫来管制,这个工夫能够通过 jdbc 的连贯参数 socketTimeout 来管制:

jdbc:mysql://127.0.0.1:3306/xxx?socketTimeout=250

这个值最终会被赋值给 HikariCP 的 networkTimeout 字段,这就是为什么最初那一步应用这个字段来还原驱动连贯超时属性的起因;说到这里,最初那里为啥要再次还原呢?这就很容易了解了,因为验证完结了,连贯对象还存活的状况下,它的 networkTimeout 的值这时依然等于 validationTimeout(不合预期),显然在拿出去用之前,须要复原老本来的值,也就是 HikariCP 里的networkTimeout 属性。

五、流程 1.1.2:敞开连贯对象

流程 1.1.2

这个流程简略来说就是把 流程 1.1.1中验证不通过的死连贯,被动敞开的一个流程,首先会把这个连贯对象从 ConnectionBag移除 ,而后把理论的物理连贯交给一个线程池去异步执行,这个线程池就是在 主流程 2 里初始化池的时候初始化的线程池 closeConnectionExecutor,而后异步工作内开始理论的关连贯操作,因为被动敞开了一个连贯相当于少了一个连贯,所以还会触发一次裁减连接池(参考 主流程 5 )操作。

六、流程 2.1:HikariCP 监控设置

不同于 Druid 那样监控指标那么多,HikariCP 会把咱们十分关怀的几项指标裸露给咱们,比方以后连接池内闲置连接数、总连接数、一个连贯被用了多久偿还、创立一个物理连贯破费多久等,HikariCP 的连接池的监控咱们这一节专门具体的合成一下,首先找到 HikariCP 上面的 metrics 文件夹,这上面搁置了一些标准实现的监控接口等,还有一些现成的实现(比方 HikariCP 自带对 prometheusmicrometerdropwizard 的反对,不太理解前面两个,prometheus下文间接称为 普罗米修斯):

图 2

上面,来着重看下接口的定义:

// 这个接口的实现次要负责收集一些动作的耗时
public interface IMetricsTracker extends AutoCloseable
{
    // 这个办法触发点在创立理论的物理连贯时(主流程 3),用于记录一个理论的物理连贯创立所消耗的工夫
    default void recordConnectionCreatedMillis(long connectionCreatedMillis) {}

    // 这个办法触发点在 getConnection 时(主流程 1),用于记录获取一个连贯时理论的耗时
    default void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) {}

    // 这个办法触发点在回收连贯时(主流程 6),用于记录一个连贯从被获取到被回收时所耗费的工夫
    default void recordConnectionUsageMillis(final long elapsedBorrowedMillis) {}

    // 这个办法触发点也在 getConnection 时(主流程 1),用于记录获取连贯超时的次数,每产生一次获取连贯超时,就会触发一次该办法的调用
    default void recordConnectionTimeout() {}

    @Override
    default void close() {}
}

触发点都理解分明后,再来看看 MetricsTrackerFactory 的接口定义:

// 用于创立 IMetricsTracker 实例,并且按需记录 PoolStats 对象里的属性(这个对象里的属性就是相似连接池以后闲置连接数之类的线程池状态类指标)public interface MetricsTrackerFactory
{
    // 返回一个 IMetricsTracker 对象,并且把 PoolStats 传了过来
    IMetricsTracker create(String poolName, PoolStats poolStats);
}

下面的接口用法见正文,针对新呈现的 PoolStats 类,咱们来看看它做了什么:

public abstract class PoolStats {
    private final AtomicLong reloadAt; // 触发下次刷新的工夫(工夫戳)private final long timeoutMs; // 刷新上面的各项属性值的频率,默认 1s,无奈扭转

    // 总连接数
    protected volatile int totalConnections;
    // 闲置连接数
    protected volatile int idleConnections;
    // 流动连接数
    protected volatile int activeConnections;
    // 因为无奈获取到可用连贯而阻塞的业务线程数
    protected volatile int pendingThreads;
    // 最大连接数
    protected volatile int maxConnections;
    // 最小连接数
    protected volatile int minConnections;

    public PoolStats(final long timeoutMs) {
        this.timeoutMs = timeoutMs;
        this.reloadAt = new AtomicLong();}

    // 这里以获取最大连接数为例,其余的跟这个差不多
    public int getMaxConnections() {if (shouldLoad()) { // 是否应该刷新
            update(); // 刷新属性值,留神这个 update 的实现在 HikariPool 里,因为这些属性值的间接或间接起源都是 HikariPool}

        return maxConnections;
    }
    
    protected abstract void update(); // 实现在↑下面曾经说了

    private boolean shouldLoad() { // 依照更新频率来决定是否刷新属性值
        for (; ;) {final long now = currentTime();
            final long reloadTime = reloadAt.get();
            if (reloadTime > now) {return false;} else if (reloadAt.compareAndSet(reloadTime, plusMillis(now, timeoutMs))) {return true;}
        }
    }
}

实际上这里就是这些属性获取和触发刷新的中央,那么这个对象是在哪里被生成并且丢给 MetricsTrackerFactorycreate办法的呢?这就是本节所须要讲述的要点:主流程 2 里的设置监控器的流程,来看看那里产生了什么事吧:

// 监控器设置办法(此办法在 HikariPool 中,metricsTracker 属性就是 HikariPool 用来触发 IMetricsTracker 里办法调用的)public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) {if (metricsTrackerFactory != null) {
        //MetricsTrackerDelegate 是包装类,是 HikariPool 的一个动态外部类,是理论持有 IMetricsTracker 对象的类,也是理论触发 IMetricsTracker 里办法调用的类
        // 这里首先会触发 MetricsTrackerFactory 类的 create 办法拿到 IMetricsTracker 对象,而后利用 getPoolStats 初始化 PoolStat 对象,而后也一并传给 MetricsTrackerFactory
        this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats()));
    } else {
        // 不启用监控,间接等于一个没有实现办法的空类
        this.metricsTracker = new NopMetricsTrackerDelegate();}
}

private PoolStats getPoolStats() {
    // 初始化 PoolStats 对象,并且规定 1s 触发一次属性值刷新的 update 办法
    return new PoolStats(SECONDS.toMillis(1)) {
        @Override
        protected void update() {
            // 实现了 PoolStat 的 update 办法,刷新各个属性的值
            this.pendingThreads = HikariPool.this.getThreadsAwaitingConnection();
            this.idleConnections = HikariPool.this.getIdleConnections();
            this.totalConnections = HikariPool.this.getTotalConnections();
            this.activeConnections = HikariPool.this.getActiveConnections();
            this.maxConnections = config.getMaximumPoolSize();
            this.minConnections = config.getMinimumIdle();}
    };
}

到这里 HikariCP 的监控器就算是注册进去了,所以要想实现本人的监控器拿到下面的指标,要通过如下步骤:

  1. 新建一个类实现 IMetricsTracker 接口,咱们这里将该类记为IMetricsTrackerImpl
  2. 新建一个类实现 MetricsTrackerFactory 接口,咱们这里将该类记为 MetricsTrackerFactoryImpl,并且将下面的IMetricsTrackerImpl 在其 create 办法 内实例化
  3. MetricsTrackerFactoryImpl 实例化后调用 HikariPool 的 setMetricsTrackerFactory 办法注册到 Hikari 连接池。

下面没有提到 PoolStats 里的属性怎么监控,这里来说下,因为 create 办法 是调用一次就没了,create 办法 只是接管了 PoolStats 对象的实例,如果不解决,那么随着 create 调用的完结,这个实例针对监控模块来说就失去持有了,所以这里如果想要拿到 PoolStats 里的属性,就须要开启一个 守护线程 ,让其持有PoolStats 对象实例,并且定时获取其外部属性值,而后 push 给监控零碎,如果是普罗米修斯等应用 pull 形式 获取监控数据的监控零碎,能够效仿 HikariCP 原生普罗米修斯监控的实现,自定义一个 Collector 对象来接管 PoolStats 实例,这样普罗米修斯就能够定期拉取了,比方 HikariCP 依据普罗米修斯监控零碎本人定义的 MetricsTrackerFactory 实现(对应 图 2 里的 PrometheusMetricsTrackerFactory 类):

@Override
public IMetricsTracker create(String poolName, PoolStats poolStats) {getCollector().add(poolName, poolStats); // 将接管到的 PoolStats 对象间接交给 Collector,这样普罗米修斯服务端每触发一次采集接口的调用,PoolStats 都会跟着执行一遍外部属性获取流程
    return new PrometheusMetricsTracker(poolName, this.collectorRegistry); // 返回 IMetricsTracker 接口的实现类
}

// 自定义的 Collector
private HikariCPCollector getCollector() {if (collector == null) {
        // 注册到普罗米修斯收集核心
        collector = new HikariCPCollector().register(this.collectorRegistry);
    }
    return collector;

通过下面的解释能够晓得在 HikariCP 中如何自定义一个本人的监控器,以及相比 Druid 的监控,有什么区别。工作中很多时候都是须要自定义的,我司尽管也是用的普罗米修斯监控,然而因为 HikariCP 原生的普罗米修斯收集器外面对监控指标的命名并 不合乎我司的标准 ,所以就 自定义 了一个,有相似问题的无妨也试一试。

🍁 这一节没有画图,纯代码,因为画图不太好解释这部分的货色,这部分内容与连接池整体流程关系也不大,充其量获取了连接池自身的一些属性,在连接池里的触发点也在下面代码段的正文里说分明了,看代码定义可能更好了解一些。

七、流程 2.2:连贯透露的检测与告警

本节对应 主流程 2 里的 子流程 2.2,在初始化池对象时,初始化了一个叫做 leakTaskFactory 的属性,本节来看下它具体是用来做什么的。

7.1:它是做什么的?

一个连贯被拿出去应用工夫超过leakDetectionThreshold(可配置,默认 0)未偿还的,会触发一个连贯透露正告,告诉业务方目前存在连贯透露的问题。

7.2:过程详解

该属性是 ProxyLeakTaskFactory 类型对象,且它还会持有 houseKeepingExecutorService 这个线程池对象,用于生产 ProxyLeakTask 对象,而后利用下面的 houseKeepingExecutorService 延时运行该对象里的 run 办法。该流程的触发点在下面的 流程 1.1最初包装成 ProxyConnection 对象的那一步,来看看具体的流程图:

流程 2.2

每次在 流程 1.1那里生成 ProxyConnection 对象时,都会触发下面的流程,由流程图能够晓得,ProxyConnection对象持有 PoolEntryProxyLeakTask的对象,其中初始化 ProxyLeakTask 对象时就用到了 leakTaskFactory 对象,通过其 schedule 办法能够进行 ProxyLeakTask 的初始化,并将其实例传递给 ProxyConnection 进行初始化赋值(ps:由图知 ProxyConnection 在触发回收事件时,会被动勾销这个透露查看工作,这也是 ProxyConnection 须要持有 ProxyLeakTask 对象的起因)。

在下面的流程图中能够晓得,只有在 leakDetectionThreshold 不等于 0 的时候才会生成一个带有理论延时工作的 ProxyLeakTask 对象,否则返回无实际意义的空对象。所以要想启用连贯透露查看,首先要把 leakDetectionThreshold 配置设置上,这个属性示意通过该工夫后借出去的连贯仍未偿还,则触发连贯透露告警。

ProxyConnection之所以要持有 ProxyLeakTask 对象,是因为它能够监听到连贯是否触发偿还操作,如果触发,则调用 cancel 办法勾销延时工作,避免误告。

由此流程能够晓得,跟 Druid 一样,HikariCP 也有连贯对象透露查看,与 Druid 被动回收连贯相比,HikariCP 实现更加简略,仅仅是在触发时打印正告日志,不会采取具体的强制回收的措施。

与 Druid 一样,默认也是敞开这个流程的,因为理论开发中个别应用第三方框架,框架自身会保障及时的 close 连贯,避免连贯对象透露,开启与否还是取决于业务是否须要,如果肯定要开启,如何设置 leakDetectionThreshold 的大小也是须要思考的一件事。

八、主流程 3:生成连贯对象

本节来讲下 主流程 2 里的 createEntry 办法,这个办法利用 PoolBase 里的 DriverDataSource 对象生成一个理论的连贯对象(如果遗记 DriverDatasource 是哪里初始化的了,能够看下 主流程 2 PoolBaseinitializeDataSource办法的作用),而后用 PoolEntry 类包装成 PoolEntry 对象,当初来看下这个包装类有哪些次要属性:

final class PoolEntry implements IConcurrentBagEntry {private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class);
    // 通过 cas 来批改 state 属性
    private static final AtomicIntegerFieldUpdater stateUpdater;

    Connection connection; // 理论的物理连贯对象
    long lastAccessed; // 触发回收时刷新该工夫,示意“最近一次应用工夫”long lastBorrowed; //getConnection 里 borrow 胜利后刷新该工夫,示意“最近一次借出的工夫”@SuppressWarnings("FieldCanBeLocal")
    private volatile int state = 0; // 连贯状态,枚举值:IN_USE(应用中)、NOT_IN_USE(闲置中)、REMOVED(已移除)、RESERVED(标记为保留中)private volatile boolean evict; // 是否被标记为废除,很多中央用到(比方流程 1.1 靠这个判断连贯是否已被废除,再比方主流程 4 里时钟回拨时触发的间接废除逻辑)private volatile ScheduledFuture<?> endOfLife; // 用于在超过连贯生命周期(maxLifeTime)时废除连贯的延时工作,这里 poolEntry 要持有该对象,次要是因为在对象被动被敞开时(意味着不须要在超过 maxLifeTime 时被动生效了),须要 cancel 掉该工作

    private final FastList openStatements; // 以后该连贯对象上生成的所有的 statement 对象,用于在回收连贯时被动敞开这些对象,避免存在漏关的 statement
    private final HikariPool hikariPool; // 持有 pool 对象

    private final boolean isReadOnly; // 是否为只读
    private final boolean isAutoCommit; // 是否存在事务
}

下面就是整个 PoolEntry 对象里所有的属性,这里再说下 endOfLife 对象,它是一个利用 houseKeepingExecutorService 这个线程池对象做的延时工作,这个延时工作个别在创立好连贯对象后 maxLifeTime 左右的工夫触发,具体来看下 createEntry 代码:

private PoolEntry createPoolEntry() {final PoolEntry poolEntry = newPoolEntry(); // 生成理论的连贯对象

        final long maxLifetime = config.getMaxLifetime(); // 拿到配置好的 maxLifetime
        if (maxLifetime > 0) { //<= 0 的时候不启用被动过期策略
            // 计算须要减去的随机数
            // 源正文:variance up to 2.5% of the maxlifetime
            final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong(maxLifetime / 40) : 0;
            final long lifetime = maxLifetime - variance; // 生成理论的延时工夫
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(() -> { // 理论的延时工作,这里间接触发 softEvictConnection,而 softEvictConnection 内则会标记该连贯对象为废除状态,而后尝试批改其状态为 STATE_RESERVED,若胜利,则触发 closeConnection(对应流程 1.1.2)if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {addBagItem(connectionBag.getWaitingThreadCount()); // 回收结束后,连接池内少了一个连贯,就会尝试新增一个连贯对象
                        }
                    },
                    lifetime, MILLISECONDS)); // 给 endOfLife 赋值,并且提交延时工作,lifetime 后触发
        }

        return poolEntry;
    }

    // 触发新增连贯工作
    public void addBagItem(final int waiting) {
        // 前排提醒:addConnectionQueue 和 addConnectionExecutor 的关系和初始化参考主流程 2

        // 当增加连贯的队列里已提交的工作超过那些因为获取不到连贯而产生阻塞的线程个数时,就进行提交连贯新增连贯的工作
        final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
        if (shouldAdd) {
            // 提交工作给 addConnectionExecutor 这个线程池,PoolEntryCreator 是一个实现了 Callable 接口的类,上面将通过流程图的形式介绍该类的 call 办法
            addConnectionExecutor.submit(poolEntryCreator);
        }
    }

通过下面的流程,能够晓得,HikariCP 个别通过 createEntry 办法来新增一个连贯入池,每个连贯被包装成 PoolEntry 对象,在创立好对象时,同时会提交一个延时工作来敞开废除该连贯,这个工夫就是咱们配置的 maxLifeTime,为了保障不在同一时间生效,HikariCP 还会利用maxLifeTime 减去一个随机数作为最终的延时工作延迟时间,而后在触发废除工作时,还会触发 addBagItem,进行连贯增加工作(因为废除了一个连贯,须要往池子里补充一个),该工作则交给由 主流程 2 里定义好的 addConnectionExecutor 线程池执行,那么,当初来看下这个异步增加连贯对象的工作流程:

addConnectionExecutor 的 call 流程

这个流程就是往连接池里加连贯用的,跟 createEntry 联合起来说是因为这俩流程是严密相干的,除此之外,主流程 5 fillPool,裁减连接池)也会触发该工作。

九、主流程 4:连接池缩容

HikariCP 会依照 minIdle 定时清理闲置过久的连贯,这个定时工作在 主流程 2 初始化连接池对象时被启用,跟下面的流程一样,也是利用 houseKeepingExecutorService 这个线程池对象做该定时工作的执行器。

来看下 主流程 2 里是怎么启用该工作的:

//housekeepingPeriodMs 的默认值是 30s,所以定时工作的距离为 30s
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

那么本节次要来说下 HouseKeeper 这个类,该类实现了 Runnable 接口,回收逻辑次要在其 run 办法内,来看看 run 办法的逻辑流程图:

主流程 4:连接池缩容

下面的流程就是 HouseKeeper 的 run 办法里具体做的事件,因为零碎工夫回拨会导致该定时工作回收一些连贯时产生误差,因而存在如下判断:

//now 就是以后零碎工夫,previous 就是上次触发该工作时的工夫,housekeepingPeriodMs 就是隔多久触发该工作一次
// 也就是说 plusMillis(previous, housekeepingPeriodMs)示意以后工夫
// 如果零碎工夫没被回拨,那么 plusMillis(now, 128)肯定是大于以后工夫的,如果被零碎工夫被回拨
// 回拨的工夫超过 128ms,那么上面的判断就成立,否则永远不会成立
if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs))

这是 hikariCP 在解决零碎时钟被回拨时做出的一种措施,通过流程图能够看到,它是间接把池子里所有的连贯对象取出来挨个儿的标记成废除,并且尝试把状态值批改为STATE_RESERVED(前面会阐明这些状态,这里先不深究)。如果零碎时钟没有产生扭转(绝大多数状况会命中这一块的逻辑),由图知,会把以后池内所有处于闲置状态(STATE_NOT_IN_USE)的连贯拿进去,而后计算须要查看的范畴,而后循环着批改连贯的状态:

// 拿到所有处于闲置状态的连贯
final List notInUse = connectionBag.values(STATE_NOT_IN_USE);
// 计算出须要被查看闲置工夫的数量,简略来说,池内须要保障最小 minIdle 个连贯活着,所以须要计算出超出这个范畴的闲置对象进行查看
int toRemove = notInUse.size() - config.getMinIdle();
for (PoolEntry entry : notInUse) {
  // 在查看范畴内,且闲置工夫超出 idleTimeout,而后尝试将连贯对象状态由 STATE_NOT_IN_USE 变为 STATE_RESERVED 胜利
  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {closeConnection(entry, "(connection has passed idleTimeout)"); // 满足上述条件,进行连贯敞开
    toRemove--;
  }
}
fillPool(); // 因为可能回收了一些连贯,所以要再次触发连接池裁减流程查看下是否须要新增连贯。

下面的代码就是流程图里对应的没有回拨零碎工夫时的流程逻辑。该流程在 idleTimeout 大于 0(默认等于 0)并且 minIdle 小于 maxPoolSize 的时候才会启用,默认是不启用的,若须要启用,能够依照条件来配置。

十、主流程 5:裁减连接池

这个流程次要附丽 HikariPool 里的 fillPool 办法,这个办法曾经在下面很多流程里呈现过了,它的作用就是在触发连贯废除、连接池连贯不够用时,发动裁减连接数的操作,这是个很简略的过程,上面看下源码(为了使代码构造更加清晰,对源码做了轻微改变):

// PoolEntryCreator 对于 call 办法的实现流程在主流程 3 里曾经看过了,然而这里却有俩 PoolEntryCreator 对象,// 这是个较细节的中央,用于打日志用,不再说这部分,为了便于了解,只须要晓得这俩对象执行的是同一块 call 办法即可
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null);
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding");

private synchronized void fillPool() {
  // 这个判断就是依据以后池子里相干数据,推算出须要裁减的连接数,// 判断形式就是利用最大连接数跟以后连贯总数的差值,与最小连接数与以后池内闲置的连接数的差值,取其最小的那一个失去
  int needAdd = Math.min(maxPoolSize - connectionBag.size(),
  minIdle - connectionBag.getCount(STATE_NOT_IN_USE));

  // 减去以后排队的工作,就是最终须要新增的连接数
  final int connectionsToAdd = needAdd - addConnectionQueue.size();
  for (int i = 0; i < connectionsToAdd; i++) {
    // 个别循环的最初一次会命中 postFillPoolEntryCreator 工作,其实就是在最初一次会打印一次日志而已(能够疏忽该烦扰逻辑)addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
  }
}

由该过程能够晓得,最终这个新增连贯的工作也是交由 addConnectionExecutor 线程池来解决的,而工作的主题也是 PoolEntryCreator,这个流程能够参考 主流程 3.

而后 needAdd 的推算:

Math.min(最大连接数 - 池内以后连贯总数, 最小连接数 - 池内闲置的连接数)

依据这个形式判断,能够保障池内的连接数永远不会超过 maxPoolSize,也永远不会低于minIdle。在连贯吃紧的时候,能够保障每次触发都以minIdle 的数量扩容。因而如果在 maxPoolSizeminIdle配置的值一样的话,在池内连贯吃紧的时候,就不会产生任何扩容了。

十一、主流程 6:连贯回收

最开始说过,最终实在的物理连贯对象会被包装成 PoolEntry 对象,寄存进 ConcurrentBag,而后获取时,PoolEntry 对象又会被再次包装成ProxyConnection 对象裸露给应用方的,那么触发连贯回收,实际上就是触发 ProxyConnection 里的 close 办法:

public final void close() throws SQLException {
  // 原正文:Closing statements can cause connection eviction, so this must run before the conditional below
  closeStatements(); // 此连贯对象在业务方应用过程中产生的所有 statement 对象,进行对立 close,避免漏 close 的状况
  if (delegate != ClosedConnection.CLOSED_CONNECTION) {leakTask.cancel(); // 勾销连贯透露查看工作,参考流程 2.2
    try {if (isCommitStateDirty && !isAutoCommit) { // 在存在执行语句后并且还关上了事务,调用 close 时须要被动回滚事务
        delegate.rollback(); // 回滚
        lastAccess = currentTime(); // 刷新 "最初一次应用工夫"}
    } finally {
      delegate = ClosedConnection.CLOSED_CONNECTION;
      poolEntry.recycle(lastAccess); // 触发回收
    }
  }
}

这个就是 ProxyConnection 里的 close 办法,能够看到它最终会调用 PoolEntry 的 recycle 办法进行回收,除此之外,连贯对象的最初一次应用工夫也是在这个时候刷新的,该工夫是个很重要的属性,能够用来判断一个连贯对象的闲置工夫,来看下 PoolEntry 的 recycle 办法:

void recycle(final long lastAccessed) {if (connection != null) {
    this.lastAccessed = lastAccessed; // 刷新最初应用工夫
    hikariPool.recycle(this); // 触发 HikariPool 的回收办法,把本人传过来
  }
}

之前有说过,每个 PoolEntry 对象都持有 HikariPool 的对象,不便触发连接池的一些操作,由上述代码能够看到,最终还是会触发 HikariPool 里的 recycle 办法,再来看下 HikariPool 的 recycle 办法:

void recycle(final PoolEntry poolEntry) {metricsTracker.recordConnectionUsage(poolEntry); // 监控指标相干,疏忽
  connectionBag.requite(poolEntry); // 最终触发 connectionBag 的 requite 办法偿还连贯,该流程参考 ConnectionBag 主流程里的 requite 办法局部
}

以上就是连贯回收局部的逻辑,相比其余流程,还是比拟简洁的。

十二、ConcurrentBag 主流程

这个类用来寄存最终的 PoolEntry 类型的连贯对象,提供了根本的增删查的性能,被 HikariPool 持有,下面那么多的操作,简直都是在 HikariPool 中实现的,HikariPool 用来治理理论的连贯生产动作和回收动作,实际操作的却是 ConcurrentBag 类,梳理下下面所有流程的触发点:

  • 主流程 2:初始化 HikariPool 时初始化 ConcurrentBag(构造方法),预热时通过createEntry 拿到连贯对象,调用 ConcurrentBag.add 增加连贯到 ConcurrentBag。
  • 流程 1.1:通过 HikariPool 获取连贯时,通过调用 ConcurrentBag.borrow 拿到一个连贯对象。
  • 主流程 6:通过 ConcurrentBag.requite 偿还一个连贯。
  • 流程 1.1.2:触发敞开连贯时,会通过 ConcurrentBag.remove 移除连贯对象,由后面的流程可知敞开连贯触发点为:连贯超过最大生命周期 maxLifeTime 被动废除、健康检查不通过被动废除、连接池缩容。
  • 主流程 3:通过异步增加连贯时,通过调用 ConcurrentBag.add 增加连贯到 ConcurrentBag,由后面的流程可知增加连贯触发点为:连贯超过最大生命周期 maxLifeTime 被动废除连贯后、连接池扩容。
  • 主流程 4:连接池缩容工作,通过调用 ConcurrentBag.values 筛选出须要的做操作的连贯对象,而后再通过 ConcurrentBag.reserve 实现对连贯对象状态的批改,而后会通过 流程 1.1.2触发敞开和移除连贯操作。

通过触发点整顿,能够晓得该构造里的次要办法,就是下面触发点里标记为 标签色 的局部,而后来具体看下该类的根本定义和次要办法:

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {

    private final CopyOnWriteArrayList<T> sharedList; // 最终寄存 PoolEntry 对象的中央,它是一个 CopyOnWriteArrayList
    private final boolean weakThreadLocals; // 默认 false,为 true 时能够让一个连贯对象在下方 threadList 里的 list 内处于弱援用状态,避免内存透露(参见备注 1)private final ThreadLocal<List<Object>> threadList; // 线程级的缓存,从 sharedList 拿到的连贯对象,会被缓存进以后线程内,borrow 时会先从缓存中拿,从而达到池内无锁实现
    private final IBagStateListener listener; // 外部接口,HikariPool 实现了该接口,次要用于 ConcurrentBag 被动告诉 HikariPool 触发增加连贯对象的异步操作(也就是主流程 3 里的 addConnectionExecutor 所触发的流程)private final AtomicInteger waiters; // 以后因为获取不到连贯而产生阻塞的业务线程数,这个在之前的流程里也呈现过,比方主流程 3 里 addBagItem 就会依据该指标进行判断是否须要新增连贯
    private volatile boolean closed; // 标记以后 ConcurrentBag 是否已被敞开

    private final SynchronousQueue<T> handoffQueue; // 这是个即产即销的队列,用于在连贯不够用时,及时获取到 add 办法里新创建的连贯对象,详情能够参考上面 borrow 和 add 的代码

    // 外部接口,PoolEntry 类实现了该接口
    public interface IConcurrentBagEntry {

        // 连贯对象的状态,后面的流程很多中央都曾经波及到了,比方主流程 4 的缩容
        int STATE_NOT_IN_USE = 0; // 闲置
        int STATE_IN_USE = 1; // 应用中
        int STATE_REMOVED = -1; // 已废除
        int STATE_RESERVED = -2; // 标记保留,介于闲置和废除之间的中间状态,次要由缩容那里触发批改

        boolean compareAndSet(int expectState, int newState); // 尝试利用 cas 批改连贯对象的状态值

        void setState(int newState); // 设置状态值

        int getState(); // 获取状态值}

    // 参考下面 listener 属性的解释
    public interface IBagStateListener {void addBagItem(int waiting);
    }

    // 获取连贯办法
    public T borrow(long timeout, final TimeUnit timeUnit) {// 省略...}

    // 回收连贯办法
    public void requite(final T bagEntry) {// 省略...}

    // 增加连贯办法
    public void add(final T bagEntry) {// 省略...}

    // 移除连贯办法
    public boolean remove(final T bagEntry) {// 省略...}

    // 依据连贯状态值获取以后池子内所有符合条件的连贯汇合
    public List values(final int state) {// 省略...}

    // 获取以后池子内所有的连贯
    public List values() {// 省略...}

    // 利用 cas 把传入的连贯对象的 state 从 STATE_NOT_IN_USE 变为 STATE_RESERVED
    public boolean reserve(final T bagEntry) {// 省略...}

    // 获取以后池子内合乎传入状态值的连贯数量
    public int getCount(final int state) {// 省略...}
}

从这个根本构造就能够略微看出 HikariCP 是如何优化传统连接池实现的了,相比 Druid 来说,HikariCP 更加偏差无锁实现,尽量避免锁竞争的产生。

12.1:borrow

这个办法用来获取一个可用的连贯对象,触发点为 流程 1.1,HikariPool 就是利用该办法获取连贯的,上面来看下该办法做了什么:

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
    // 源正文:Try the thread-local list first
    final List<Object> list = threadList.get(); // 首先从以后线程的缓存里拿到之前被缓存进来的连贯对象汇合
    for (int i = list.size() - 1; i >= 0; i--) {final Object entry = list.remove(i); // 先移除,回收办法那里会再次 add 进来
        final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry; // 默认不启用弱援用
        // 获取到对象后,通过 cas 尝试把其状态从 STATE_NOT_IN_USE 变为 STATE_IN_USE,留神,这里如果其余线程也在应用这个连贯对象,// 并且胜利批改属性,那么以后线程的 cas 会失败,那么就会持续循环尝试获取下一个连贯对象
        if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {return bagEntry; //cas 设置胜利后,示意以后线程绕过其余线程烦扰,胜利获取到该连贯对象,间接返回}
    }

    // 源正文:Otherwise, scan the shared list ... then poll the handoff queue
    final int waiting = waiters.incrementAndGet(); // 如果缓存内找不到一个可用的连贯对象,则认为须要“回源”,waiters+1
    try {for (T bagEntry : sharedList) {
            // 循环 sharedList,尝试把连贯状态值从 STATE_NOT_IN_USE 变为 STATE_IN_USE
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                // 源正文:If we may have stolen another waiter's connection, request another bag add.
                if (waiting > 1) { // 阻塞线程数大于 1 时,须要触发 HikariPool 的 addBagItem 办法来进行增加连贯入池,这个办法的实现参考主流程 3
                    listener.addBagItem(waiting - 1);
                }
                return bagEntry; //cas 设置胜利,跟下面的逻辑一样,示意以后线程绕过其余线程烦扰,胜利获取到该连贯对象,间接返回
            }
        }

        // 走到这里阐明不光线程缓存里的列表竞争不到连贯对象,连 sharedList 里也找不到可用的连贯,这时则认为须要告诉 HikariPool,该触发增加连贯操作了
        listener.addBagItem(waiting);

        timeout = timeUnit.toNanos(timeout); // 这时候开始利用 timeout 管制获取工夫
        do {final long start = currentTime();
            // 尝试从 handoffQueue 队列里获取最新被加进来的连贯对象(个别新入的连贯对象除了加进 sharedList 之外,还会被 offer 进该队列)final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            // 如果超出指定工夫后依然没有获取到可用的连贯对象,或者获取到对象后通过 cas 设置胜利,这两种状况都不须要重试,间接返回对象
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {return bagEntry;}
            // 走到这里阐明从队列内获取到了连贯对象,然而 cas 设置失败,阐明又该对象又被其余线程率先拿去用了,若工夫还够,则再次尝试获取
            timeout -= elapsedNanos(start); //timeout 减去耗费的工夫,示意下次循环可用工夫
        } while (timeout > 10_000); // 剩余时间大于 10s 时才持续进行,个别状况下,这个循环只会走一次,因为 timeout 很少会配的比 10s 还大

        return null; // 超时,依然返回 null
    } finally {waiters.decrementAndGet(); // 这一步进来后,HikariPool 收到 borrow 的后果,算是走出阻塞,所以 waiters-1
    }
}

认真看下正文,该过程大抵分成三个次要步骤:

  1. 从线程缓存获取连贯
  2. 获取不到再从 sharedList 里获取
  3. 都获取不到则触发增加连贯逻辑,并尝试从队列里获取新生成的连贯对象

12.2:add

这个流程会增加一个连贯对象进入 bag,通常由 主流程 3 里的 addBagItem 办法通过 addConnectionExecutor 异步工作触发增加操作,该办法主流程如下:

public void add(final T bagEntry) {sharedList.add(bagEntry); // 间接加到 sharedList 里去

    // 源正文:spin until a thread takes it or none are waiting
    // 参考 borrow 流程,当存在线程期待获取可用连贯,并且以后新入的这个连贯状态依然是闲置状态,且队列里无消费者期待获取时,发动一次线程调度
    while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { // 留神这里会 offer 一个连贯对象入队列
        yield();}
}

联合 borrow 来了解的话,这里在存在期待线程时会增加一个连贯对象入队列,能够让 borrow 里产生期待的中央更容易 poll 到这个连贯对象。

12.3:requite

这个流程会回收一个连贯,该办法的触发点在 主流程 6 ,具体代码如下:

public void requite(final T bagEntry) {bagEntry.setState(STATE_NOT_IN_USE); // 回收意味着应用结束,更改 state 为 STATE_NOT_IN_USE 状态

    for (int i = 0; waiters.get() > 0; i++) { // 如果存在期待线程的话,尝试传给队列,让 borrow 获取
        if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {return;}
        else if ((i & 0xff) == 0xff) {parkNanos(MICROSECONDS.toNanos(10));
        }
        else {yield();
        }
    }

    final List<Object> threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) { // 线程内连贯汇合的缓存最多 50 个,这里回收连贯时会再次加进以后线程的缓存里,不便下次 borrow 获取
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); // 默认不启用弱援用,若启用的话,则缓存汇合里的连贯对象没有内存泄露的危险
    }
}

12.4:remove

这个负责从池子里移除一个连贯对象,触发点在 流程 1.1.2,代码如下:

public boolean remove(final T bagEntry) {
    // 上面两个 cas 操作,都是从其余状态变为移除状态,任意一个胜利,都不会走到上面的 warn log
    if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
        return false;
    }

    // 间接从 sharedList 移除掉
    final boolean removed = sharedList.remove(bagEntry);
    if (!removed && !closed) {LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
    }

    return removed;
}

这里须要留神的是,移除时仅仅移除了 sharedList 里的对象,各个线程内缓存的那一份汇合里对应的对象并没有被移除,这个时候会不会存在该连贯再次从缓存里拿到呢?会的,然而不会返回进来,而是间接 remove 掉了,认真看 borrow 的代码发现状态不是闲置状态的时候,取出来时就会 remove 掉,而后也拿不进来,天然也不会触发回收办法。

12.5:values

该办法存在重载办法,用于返回以后池子内连贯对象的汇合,触发点在 主流程 4 ,代码如下:

public List values(final int state) {
    // 过滤出来合乎状态值的对象汇合逆序后返回进来
    final List list = sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList());
    Collections.reverse(list);
    return list;
}

public List values() {
    // 返回全副连贯对象(留神下方 clone 为浅拷贝)return (List) sharedList.clone();}

12.6:reserve

该办法单纯将连贯对象的状态值由 STATE_NOT_IN_USE 批改为 STATE_RESERVED,触发点依然是 主流程 4 ,缩容时应用,代码如下:

public boolean reserve(final T bagEntry){return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
}

12.7:getCount

该办法用于返回池内合乎某个状态值的连贯的总数量,触发点为 主流程 5 ,裁减连接池时用于获取闲置连贯总数,代码如下:

public int getCount(final int state){
   int count = 0;
   for (IConcurrentBagEntry e : sharedList) {if (e.getState() == state) {count++;}
   }
   return count;
}

以上就是 ConcurrentBag 的次要办法和解决连贯对象的次要流程。

十三、总结

到这里基本上一个连贯的生产到获取到回收到废除一整个生命周期在 HikariCP 内是如何治理的就说完了,相比之前的 Druid 的实现,有很大的不同,次要是 HikariCP 的 无锁 获取连贯,本篇没有波及 FastList 的阐明,因为从连贯治理这个角度的确很少用到该构造,用到 FastList 的中央次要在存储连贯对象生成的 statement 对象 以及用于存储线程内缓存起来的连贯对象;

除此之外 HikariCP 还利用 javassist 技术编译期生成了 ProxyConnection 的初始化,这里也没有相干阐明,网上无关 HikariCP 的优化有很多文章,大多数都提到了 字节码优化 fastListconcurrentBag 的实现,本篇次要通过深刻解析 HikariPoolConcurrentBag的实现,来阐明 HikariCP 相比 Druid 具体做了哪些不一样的操作。

正文完
 0