后面做了n多筹备,包含同步队列、阻塞队列、线程池、周期性线程池等等,明天终于能够开始深入研究连接池了,从HikariPool开始。
连接池存在的起因和线程池大略相似,因为数据库连贯的获取是有开销的,频繁获取、敞开数据库连贯会带来不必要的开销,影响零碎性能。所以就有了数据库连接池。
数据库连接池技术提前创立好数据库连贯,并对数据库连贯进行池化治理:连贯创立好之后交给连接池,利用须要连贯的时候间接从连接池获取、而不须要创立连贯,应用完之后归还给连接池、而不是敞开连贯。从而缩小频繁创立、敞开数据库连贯锁带来的开销,极大进步零碎性能。
明天从HikariPool开始,从源码角度学习理解数据库连接池的实现原理。HikariPool是目前公认性能最好的数据库连接池,同时也是SpringBoot2.0当前的默认连接池,置信今后会有越来越多的公司和我的项目应用HikariPool连接池。
对于数据库连接池的根本认知
先对数据库连接池的根本工作原理做个理解,不论是HikariPool、还是druid,所有的数据库连接池应该都是依照这个基本原理工作和实现的,带着这个思路去学习数据库连接池,防止盲人摸象。
数据库连接池肯定会蕴含以下根本逻辑:
- 创立连贯并池化:初始化的时候创立、或者是在利用获取连贯的过程中创立,连贯创立好之后放在连接池(内存中的容器,比方List)中保留。
- 获取数据库连贯:接管了获取数据库连贯的办法,从连接池中获取、而不是创立连贯。
- 敞开数据库连贯:接管了敞开数据库连贯的办法,将连贯偿还到连接池、而不是真正的敞开数据库连贯。
- 连接池保护:连接池容量、连贯超时清理等工作。
带着这个思路钻研HikariPool的源码,会有事倍功半的效用。
意识HikariPool的构造
包含以下几个局部:
- ConcurrentBag & PoolEntry
- addConnectionExecutor
- houseKeeperTask
- HikariProxyConnection
- closeConnectionExecutor
ConcurrentBag
直译就是“连贯口袋”,就是放数据库连贯的口袋,也就是连接池。
ConcurrentBag有3个保留数据库连贯的中央(池):
- threadList:是一个ThreadLocal变量,保留以后线程持有的数据库连贯。
- sharedList:是一个CopyOnWriteArrayList,线程平安的arrayList,数据库连贯创立后首先进入sharedList。
- handoffQueue:是一个SynchronousQueue,数据库连贯创立、进入sharedList后,也会进入handoffQueue期待连贯申请。
除此之外,ConcurrentBag还有一个比拟重要的属性waiters,记录向连接池获取数据库连贯而不得、期待的线程数。
PoolEntry
连接池中存储的对象不是Connection,也不是Connection的代理对象,而是PoolEntry。
PoolEntry持有数据库连贯对象Connection,Connection在实例化PoolEntry前创立。PoolEntry创立的过程中会同时创立两个ScheduledFuture工作:endOfLife和keepalive。
endOfLife提交MaxLifetimeTask定时工作,在PoolEntry创立后的maxLifetime(参数指定)执行。MaxLifetimeTask会敞开以后连贯、同时依据须要创立新的连贯退出到连接池。
keepalive提交KeepaliveTask周期性工作,在PoolEntry创立后依照keepalive(参数设定)周期性执行,在以后连贯闲暇的状况下查看连贯是否可用(应用参数指定的ConnectionTestQuery进行测试),如果连贯不可用则敞开并从新创立连贯。
addConnectionExecutor
增加数据库连贯的工作管理器。负责数据库连贯的创立以及退出到连接池中(sharedList和handoffQueue)。
留神addConnectionExecutor自身是一个线程池ThreadPoolExecutor,线程池容量为最大数据库连接数,HikariPool初始化的过程中会以多线程(corePoolSize=CPU内核数量)的形式疾速实现连贯创立和池化,之后失常状况下会以单线程(corePoolSize=1)的形式创立数据库连贯。
houseKeeperTask
是一个周期性线程池ScheduledExecutorService,初始化的时候创立,定时(housekeepingPeriodMs)执行,清理掉(敞开)连接池中多余的(大于最小闲暇数)闲暇连贯,如果连接池中的连接数没有达到参数设置的数量(最大连接数、或者闲暇连贯没有达到最小闲暇连接数)则创立连贯。
HikariProxyConnection
HikariProxyConnection是数据库连贯Connection的扩大类,应用层通过getConnection办法获取到的数据库连贯其实不是不是数据库连贯Connection、而是这个扩大类HikariProxyConnection。
应用扩大类HikariProxyConnection、而不是原生的数据库连贯Connection的一个最直观的理由是,对于数据库连接池来说,连贯的敞开办法close不是要敞开连贯、而是要把连贯交还给连接池。应用扩大类(或者代理类)就能够很容易的重写其close办法实现目标,而如果间接应用原生Connection的话,就没方法管制了。
closeConnectionExecutor
数据库连接池须要敞开的时候,通过closeConnectionExecutor线程池提交,敞开连贯后closeConnectionExecutor还负责调用fillPool(),依据须要填充连接池。
好了,HikariPool的根底构造理解完了,接下来要进入源码剖析了,次要包含:
- HikariPool的初始化
- 获取数据库连贯 - getConnection办法
- 敞开数据库连贯 - close办法
HikariPool的初始化
要开始剖析源码了。
HikariPool是实例化的时候在构造方法中实现初始化。代码尽管不是很多,然而内容很多,所以还是分段、一步一步剖析。
public HikariPool(final HikariConfig config) { super(config); this.connectionBag = new ConcurrentBag<>(this); this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK; this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); checkFailFast();
首先调用super也就是PoolBase的构造方法,次要设置配置的相干参数,初始化DataSource(initializeDataSource,获取到数据库连贯的url、username、password等重要参数,创立DataSource)。
之后创立ConcurrentBag、实现ConcurrentBag的初始化(通过构造方法):创立handoffQueue、sharedList以及threadList。
接下来初始化houseKeepingExecutorService,也就是创立一个ScheduledThreadPoolExecutor,筹备接管houseKeep工作。
而后调用checkFailFast(),checkFailFast()的作用是在初始化连接池之前首先做一次疾速尝试:创立一个PoolEntry(通过createPoolEntry办法,稍后剖析源码),如果创立胜利则将其退出连接池后,返回,否则如果失败(数据库连贯创立失败)则一直尝试直到消耗完设置的初始化工夫initializationTimeout。
接着看初始化代码:
if (config.getMetricsTrackerFactory() != null) { setMetricsTrackerFactory(config.getMetricsTrackerFactory()); } else { setMetricRegistry(config.getMetricRegistry()); } setHealthCheckRegistry(config.getHealthCheckRegistry()); handleMBeans(this, true); ThreadFactory threadFactory = config.getThreadFactory(); final int maxPoolSize = config.getMaximumPoolSize(); LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize); this.addConnectionQueueReadOnlyView = unmodifiableCollection(addConnectionQueue); this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy()); this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService); this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
以上代码次要实现以下几件事件:
MetricsTrackerFactory次要用来创立数据库连贯的统计分析工作。
注册健康检查工作,默认是CodahaleHealthChecker,负责连贯的健康检查,明天暂不波及。
JMX解决,暂不波及。
最要害的局部来了:创立addConnectionExecutor,addConnectionQueueReadOnlyView,closeConnectionExecutor,leakTaskFactory以及houseKeeperTask。
这几个线程池都十分重要。
addConnectionExecutor负责创立连贯、封装到PoolEntry中之后退出连接池中。houseKeeperTask负责连接池清理、连接池中的连贯数量没有达到参数设置的连贯数量的话,houseKeeperTask还负责创立连贯。leakTaskFactory负责创立连贯泄露查看工作。
HikariPool连接池初始化的大部分工作都在以上几行代码中:
首先来看houseKeeperTask,HouseKeeperTask提交HouseKeeper工作,HouseKeeper是实现了Runnable接口的HikariPool的外部类,HouseKeeper查看以后闲暇连接数如果大于参数设置的最小闲暇连接数的话,会把超过idleTimeout未用的连贯敞开。之后调用fillPool()办法。
fillPool()办法查看以后连接池中的连接数没有达到参数设置的最大连接数、或者闲暇连接数没有达到参数设置的最小闲暇连接数的话,通过调用addConnectionExecutor.submit办法提交poolEntryCreator工作、创立n个连贯(并退出连接池)直到连贯数量满足参数设置的要求。
PoolEntryCreator是实现了callable接口的工作,提交给线程池addConnectionExecutor之后,addConnectionExecutor会调用PoolEntryCreator的call()办法,call()办法调用createPoolEntry()办法创立数据库连贯、创立之后调用connectionBag.add办法将新创建的PoolEntry退出连接池。
代码看到这里,咱们能够得出一个论断:houseKeeperTask会通过addConnectionExecutor提交多个(参数最大连接数、最小闲暇连接数指定)创立数据库连贯的工作,从而实现数据库连接池中初始化连贯的创立!
接下来,咱们再看一下初始化的最初一段代码:
if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) { addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors())); addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors())); final long startTime = currentTime(); while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) { quietlySleep(MILLISECONDS.toMillis(100)); } addConnectionExecutor.setCorePoolSize(1); addConnectionExecutor.setMaximumPoolSize(1); }
这段代码的意思是:如果参数blockUntilFilled设置为true(连接池没有实现初始化则阻塞)、并且参数InitializationFailTimeout>1(初始化阻塞时长)的话,则设置addConnectionExecutor的最大线程数和外围线程数为16和内核数量的最大值,阻塞期待初始化时长达到参数设定值、或者连接池中创立的连贯数量曾经大于最小闲暇数量后,从新设置addConnectionExecutor的外围线程数和最大线程数为1。
这段代码要达到的指标是:在参数设定的初始化时长范畴内,将addConnectionExecutor线程池加大到最大马力(线程数设置到尽可能最大)、以最短的工夫实现初始连贯的创立。初始连贯创立实现之后,将addConnectionExecutor的线程数复原为1,也就是说初始化的时候调动尽可能多的线程创立连贯、初始化实现之后的连贯创立实际上是由一个线程实现的。
初始化的代码剖析结束,然而为了可读性,咱们跳过了createPoolEntry办法的源码,创立物理连贯就是在createPoolEntry办法实现的,所以createPoolEntry办法也十分重要。
当初补上!
createPoolEntry办法
createPoolEntry是HikariPool连接池中惟一创立数据库连贯的中央,通过线程池addConnectionExecutor绑定的PoolEntryCreator发动调用。
private PoolEntry createPoolEntry() { try { final PoolEntry poolEntry = newPoolEntry(); final long maxLifetime = config.getMaxLifetime(); if (maxLifetime > 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(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS)); } final long keepaliveTime = config.getKeepaliveTime(); if (keepaliveTime > 0) { // variance up to 10% of the heartbeat time final long variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10); final long heartbeatTime = keepaliveTime - variance; poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS)); } return poolEntry; } catch (ConnectionSetupException e) { if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently logger.error("{} - Error thrown while acquiring connection from data source", poolName, e.getCause()); lastConnectionFailure.set(e); } } catch (Exception e) { if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently logger.debug("{} - Cannot acquire connection from data source", poolName, e); } } return null; }
首先创立PoolEntry,而后通过线程池houseKeepingExecutorService绑定endOfLife以及keepalive工作。这两个工作的作用请参考本文的PoolEntry局部的形容。
咱们还是没有看到创立数据库物理连贯的中央,别急,他就在办法的第一行代码:newPoolEntry()中,咱们看一下newPoolEntry办法。
PoolEntry newPoolEntry() throws Exception { return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit); }
创立数据库物理连贯,交给PoolEntry的构造方法去创立PoolEntry,最终PoolEntry对象会持有创立的数据库连贯。
PoolEntry创立好之后,会调用connectionBag.add办法将其退出的连接池中。
ConcurrentBag#add
线程池addConnectionExecutor创立PoolEntry之后,会调用ConcurrentBag的add办法将其退出到连接池中:
public void add(final T bagEntry) { if (closed) { LOGGER.info("ConcurrentBag has been closed, ignoring add()"); throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()"); } sharedList.add(bagEntry); // spin until a thread takes it or none are waiting while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { Thread.yield(); } }
首先退出到sharedList中。
之后如果连接池有期待获取连贯的线程、并且以后连贯处于闲暇状态,则提交以后PoolEntry给handoffQueue队列。
handoffQueue是手递手队列,如果以后工夫点并没有排队想要从handoffQueue获取连贯的线程存在的话,以后线程会挂起期待。这里所说的“以后线程”就是“创立数据库连贯”的线程,是addConnectionExecutor线程池中的线程。
addConnectionExecutor中的线程在创立数据库连贯poolEntry之后,有以下可能:
- 以后正好有利用须要获取数据库连贯,并且通过thredList和shareList都没有获取到连贯、须要到handoffQueue获取的话,则以后刚创立的数据库连贯通过handoffQueue交给获取线程后,以后线程偿还到addConnectionExecutor线程池中,如果有多个poolEntry在handoffQueue排队的话,以后线程yield期待
- 以后没有须要排队获取连贯的线程(waiter=0),则poolEntry退出到shareList中之后,以后线程间接偿还到addConnectionExecutor线程池中
小结
HikariPool的根本框架以及初始化过程、数据库连贯的创立以及退出池中、连接池的houseKeep过程的源码剖析结束。剩下的就是应用层从池中获取连贯、以及敞开连贯的逻辑了,下一篇文章剖析。
Thanks a lot!
上一篇 Java并发编程 Lock Condition & ReentrantLock(二)