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

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是哪里初始化的了,能够看下主流程2PoolBaseinitializeDataSource办法的作用),而后用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联合起来说是因为这俩流程是严密相干的,除此之外,主流程5fillPool,裁减连接池)也会触发该工作。

九、主流程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具体做了哪些不一样的操作。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理