优化原理
获取连接
HikariCP 可以被理解为一个简单的 BlockingQueue,它实际上并没有使用 BlockingQueue,而是使用了一种被称为 ConcurrentBag 的专用集合,但在行为概念上是类似的。在这个 BlockingQueue 的模型上,可以想象你的客户端调用 HikariDataSource 的 getConnection 方法执行着如下的工作:
public Connection getConnection() throws SQLException {
return connectionQueue.poll(timeout);
}
显然会有更多的事情发生,比如抛出超时异常等,如上是基本的思路。调用 Connection.close 方法实质上返回到池的连接,同样,还有很多具体细节,但概念是相通的。
public void close() throws SQLException {
connectionQueue.put(this);
}
HikariCP 确实有几个自己的线程,有一个 HouseKeeper 管家线程定期运行以退出空闲连接,有一个调度线程可以退出到达 maxLifetime 的连接,还有一个用于添加连接的线程,以及一个用于关闭它们的线程等。接下来,我们从获取连接部分开始仔细研究源码级的原理。
获取连接是 HikariCP 的核心功能,HikariDataSource 对象首先通过 getConnection 方法获得 HikariPool 真正的 getConnection 方法,HikariPool 内部通过我们介绍过的 ConcurrentBag 的 borrow 方法获取 PoolEntry,它将首先尝试从线程的 ThreadLocal 最近使用的连接列表中获取未使用的连接。最后通过 PoolEntry 执行 Create Proxy Connection 方法创建一个物理连接并返回其代理连接 Proxy Connection。具体的时序图如图所示。
HikariDataSource,顾名思义,就是 HikariCP 对外提供给用户的定制化的 DataSource,使用 Spring 的用户可以直接将它作为数据源。用户也可以直接初始化 HikariDataSource:
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/simpsons");
ds.setUsername("bart");
ds.setPassword("51mp50n");
HikariDataSource 继承了 HikariConfig,并且实现了 JDCB 基础中介绍过的 javax.sql 扩展包中的 DataSource 接口。作为 DriverManager 设施的替代项,DataSource 对象是获取连接的首选方法,实现 DataSource 接口的对象通常在基于 JavaTM Naming and Directory Interface (JNDI) API 的命名服务中注册。
HikariConfig 是 HikariCP 的配置管理核心类,它实现了 HikariConfigMXBean 接口,用来对外暴露 HikariCP 配置相关的 JMX 监控和管理功能。
在创建连接的过程中,有几个细节性的问题可以从源码级重点关注一下。
细节:单例池初始化
HikariDataSource 获取连接 getConnection 的单例处理。大家都知道,在实现单例模式时,如果未考虑多线程的情况,很可能会造成实例化了多次并且被不同对象持有。在 JDK1.5 或者更晚的版本中,扩展了 volatile 的语义,使用了 volatile 关键字后,重排序被禁止,双重检查锁可以减少开销。如果没有 volatile 关键字则可能由于指令重排导致 HikariPool 对象 pool 在多线程下无法正确地初始化,volatile 禁止了指令重排,并强制本地线程读取主存。由于数据库连接池处于一个激烈的被频繁调用的位置,HikariCP 的源码部分就使用双重检查锁进行单例初始化。以下是 HikariDataSource 中的部分源码代码:
private final HikariPool fastPathPool;
private volatile HikariPool pool; //注意这里引入了volatile
public Connection getConnection() throws SQLException {
if (isClosed()) //检查数据源是否已经关闭,如果关闭则抛出异常
{ throw new SQLException("HikariDataSource " + this + " has been
closed."); }
if (fastPathPool ! = null) {//如果当前引用HikariPool不为空,则直接返回连接
return fastPathPool.getConnection();
}
HikariPool result = pool;
if (result == null) {//典型的双重检验锁代码
synchronized (this) {
result = pool;
if (result == null) {
validate(); //参数校验,主要是检查参数是否合法并给予默认值
try {
pool = result = new HikariPool(this); //初始化连接池
this.seal();
}
catch (PoolInitializationException pie) {
if (pie.getCause() instanceof SQLException) {
throw (SQLException) pie.getCause();
}
else {
throw pie;
}
}
LOGGER.info("{} - Start completed.", getPoolName());
}
}
}
//从连接池中返回一个连接,返回给HikariPool资源池与ConcurrentBag进行交互
return result.getConnection();
}
细节:连接有效性
另一个细节是获取连接时的有效性检查。当从资源池里面获取到资源后,需要检查该资源的有效性,如果失效,则再次获取连接,这样可以避免执行业务的时候报错。这部分的工作是由 HikariPool 做的,它通过判断 PoolEntry 是否已经被标记清除了、当前 PoolEntry 的存活时间是否过期及当前连接是否活着 3 项进行判断,如果超时则关闭这个 PoolEntry 的连接、重置超时时间、再次获取连接。这部分的核心实现在 HikariPool 的 getConnection 方法里,如下所示:
try {
long timeout = hardTimeout;
do {//从connectionBag中借用连接,借用过程中会发生创建连接、连接过期、空闲等事情
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; //中断,跳出循环,并抛出异常
}
final long now = currentTime(); //记录当前时间
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && ! isConnectionAlive(poolEntry.connection))) {//有效性检查
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_ CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); //关闭当前失效连接
timeout = hardTimeout - elapsedMillis(startTime); //重置超时时间
}
else {
//借用的连接若未过期未丢弃进入此逻辑,则设置metrics监控指标
metricsTracker.recordBorrowTimeoutStats(startTime);
//最核心部分,通过代理创建连接
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
}
} while (timeout > 0L); //只要超时时间大于0则不断重试
throw createTimeoutException(startTime); //抛出创建连接超时异常
}
在上述代码中,有效性检查的部分是 isConnectionAlive,在这里用户可以自行配置心跳语句的检测,如果追求极致性能,那么使用 JDBC4 的用户还是强烈建议不要配置心跳语句,而是采用 HikariCP 默认的 com.mysql.jdbc. JDBC4Connection 的 isValid 实现,因为它的实现是 ping 命令,一般来说原生 ping 命令的性能是 select 1 的两倍。刚才我们看到的 HikariPool 的 getConnection 方法,其心跳语句检测的 isConnectionAlive 是在其父类中实现的,核心代码如下:
boolean isConnectionAlive(final Connection connection)
{
try {
try {
setNetworkTimeout(connection, validationTimeout);
final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
if (isUseJdbc4Validation) {//如果是JDBC4,则直接使用ping命令进行验证
return connection.isValid(validationSeconds); //性能可以大大提升
}
try (Statement statement = connection.createStatement()) {
if (isNetworkTimeoutSupported ! = TRUE) {
setQueryTimeout(statement, validationSeconds);
}
statement.execute(config.getConnectionTestQuery()); //否则执行测试语句验证
}
}
finally {
setNetworkTimeout(connection, networkTimeout);
if (isIsolateInternalQueries && ! isAutoCommit) {
connection.rollback();
}
}
return true;
}
catch (Exception e) {
lastConnectionFailure.set(e);
logger.warn("{} - Failed to validate connection {} ({}). Possiblyconsider using a shorter maxLifetime value.",
poolName, connection, e.getMessage());
return false;
}
}
上述代码中有一个 validationTimeout 属性,默认值是 5000 毫秒,所以默认情况下 final int validationSeconds = (int)Math.max(1000L, validationTimeout) / 1000
它的值应该为 1~5 秒。又由于 validationTimeout 的值必须小于 connectionTimeout(默认值30000毫秒,如果小于250毫秒,则被重置回30秒),所以默认情况下,调整 validationTimeout 却不调整 connectionTimeou t情况下,validationSeconds 的默认峰值应该是 30 毫秒。
如果是 JDBC4 的话,使用 isUseJdbc4Validation(就是 config.getConnectionTestQuery()== null
的时候),用 connection.isValid(validationSeconds)
来验证连接的有效性,否则的话则用 connectionTestQuery 查询语句来查询验证。
细节:创建形式
第 3 个细节就是连接的创建。连接的创建可以同步创建,也可以异步创建,这与 HikariCP 的配置息息相关,这也是 HikariCP 作者别具匠心的设计之处。
过程总结
- 实现了 JDBC 扩展包 javax.sql 的 DataSource 接口的 HikariDataSource,执行 getConnection 方法时会进行 Double-checked_locking 单例、配置检查等流程,再向 HikariPool 资源池请求获取连接。
- HikariPool 则向其 lock-free 的资源集合 ConcurrentBag 借用 PoolEntry,若没有 poolEntry 则超时抛出异常,若有 poolEntry 则创建一个 JDBC 代理连接 ProxyConnection。
- ProxyConnection 是由 ProxyFactory 产生的,它是一个生产标准 JDBC 接口代理的工厂类,HikariDataSource 最终获取的 Connection 连接就是代理工厂返回的 JDBC 物理连接,而 poolEntry 就是对这个物理连接的一对一封装。
连接有效性
Java.sql.Connection 的 isValid() 和 isClosed() 区别:
- isValid:如果连接尚未关闭并且仍然有效,则返回true。驱动程序将提交一个关于该连接的查询,或者使用其他某种能确切验证在调用此方法时连接是否仍然有效的机制。由驱动程序提交的用来验证该连接的查询将在当前事务的上下文中执行。
- 参数:timeout。等待用来验证连接是否完成的数据库操作的时间,以秒为单位。如果在操作完成之前超时期满,则此方法返回 false。0值表示不对数据库操作应用超时值。
- 返回:如果连接有效,则返回 true,否则返回 false。
- isClosed:查询此 Connection 对象是否已经被关闭。如果在连接上调用了 close 方法或者发生某些严重的错误,则连接被关闭。只有在调用了 Connection.close 方法之后被调用时,此方法才保证返回 true。通常不能调用此方法确定到数据库的连接是有效的还是无效的。通过捕获在试图进行某一操作时可能抛出的异常,典型的客户端可以确定某一连接是无效的。
- 返回:如果此 Connection 对象是关闭的,则返回 true;如果它仍然处于打开状态,则返回 false。
连接监控
HikariPool 继承了 PoolBase,实现了 HikariPoolMXBean 接口用于对外暴露 HikariCP 连接池相关的监控管理功能:
归还连接
连接的归还和连接的借用是两个大致相反的过程,归还部分的源码如下所示:
public final void close() throws SQLException
{
closeStatements(); //由于关闭语句可能导致连接被驱逐,所以优先执行
if (delegate ! = ClosedConnection.CLOSED_CONNECTION) {
leakTask.cancel();
try {
if (isCommitStateDirty && ! isAutoCommit) {
delegate.rollback(); //如果存在脏提交或者没有自动提交,则连接回滚
lastAccess = currentTime();
LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", poolEntry.getPoolName(), delegate);
}
if (dirtyBits ! = 0) {
poolEntry.resetConnectionState(this, dirtyBits);
lastAccess = currentTime();
}
delegate.clearWarnings();
}
catch (SQLException e) {
//当连接中止时,通常会抛出不适用于应用程序的异常
if (! poolEntry.isMarkedEvicted()) {
throw checkException(e);
}
}
finally {//这里开始调用PoolEntry的recycle方法
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess);
}
}
}
在 ProxyConnection 代理层获取到的连接,进行归还时调用了代理层的 close 方法。HikariCP 归还连接是一系列没有返回值的 void 操作,ProxyConnection 的 close 方法并没有直接调用 JDBC 的 close 方法,而是依次调用了 PoolEnry 的 recycle 方法、HikariPool 的 recycle 方法及 ConcurrentBag 的 requite 方法,这一系列方法传递的参数都是 PoolEntry。
数据库连接池 HikariCP 的 close 方法返回的连接其实是封装在这个 ProxyConnection 代理连接中的,当调用它的时候,它只返回与池的连接,但是依然保持与数据库的基础连接是打开的。数据库连接池通常都是以这种方式工作的,数据库连接池的性能优势就是来自于连接保持打开,通过代理连接,对用户来说是透明的。如果需要关闭这个连接,可以将它先转换为 ProxyConnection,然后调用它的 unwrap 方法,最后关闭这个内部连接。
public final <T> T unwrap(Class<T> iface) throws SQLException{
//ProxyConnection内部提供的unwrap方法
if (iface.isInstance(delegate)) {
return (T) delegate;
} else if (delegate ! = null) {
return delegate.unwrap(iface);
}
throw new SQLException("Wrapped connection is not an instance of " + iface);
}
unwrap 是一个很有意思的工具,如果你想跳过代理获取数据库的源信息,那么还可以直接使用 JDBC 的 unwrap 的 API 来获取(这里不是 ProxyConnection 的 unwrap )。如下所示:
((ConnectionProperties) connection.unwrap(ConnectionProperties.class))
.setNullCatalogMeansCurrent(false);
在归还连接的代码中,也存在很多细节。比如代理层的 close 方法第 1 行就这么讲究:
closeStatements(); //由于关闭语句可能导致连接被驱逐,所以优先执行
本书前面章节介绍的 HikariCP 诞生的理由——很多数据库连接池违反了 JDBC 规范。当 Connection 连接 close 或者 return 时,清除警告或回滚未提交的事务时,一些数据库连接池并不会自动关闭语句 Statement,并且它们也不会重置用户更改过的属性,如自动提交或事务隔离级别等,从而导致下一个消费者获得“脏”连接。
JDBC 最佳实践中有一条就是最顺序关闭 ResultSet、Statement、Connection。因此,在 HikariCP 中,ProxyConnection 的源码中,它的 close 方法覆盖了 java.sql.Connection 的方法,覆盖 JDBC 的 close 方法时,第一件事就是关闭了 Statement 语句。
private synchronized void closeStatements(){
final int size = openStatements.size();
if (size > 0) {
for (int i = 0; i < size && delegate ! = ClosedConnection.CLOSED_CONNECTION; i++){
try (Statement ignored = openStatements.get(i)) {
//自动资源清理
}
catch (SQLException e) {
LOGGER.warn("{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",poolEntry.getPoolName(), delegate);
leakTask.cancel();
poolEntry.evict("(exception closing Statements during Connection.close())");
delegate = ClosedConnection.CLOSED_CONNECTION;
}
}
openStatements.clear();
}
}
在上述 closeStatements 的具体方法实现里,在执行 clear 之前,将所有不是关闭状态的 Statement 都遍历了一遍,进行了资源的自动清理,对于遇到异常的连接,PoolEntry 对象标记具体问题原因是关闭 Statement 时产生的,并将连接设置为关闭状态。
从 ProxyConnection 覆盖 JDBC 的 close 方法,一步步调用到了 ConcurrentBag 的 requite 方法放回,就像后者方法前的注释说的那样:“有借必有还。如果你只借不还,那么就会导致内存泄漏。”
结合连接时序图和归还连接时序图我们可以发现,PoolEntry 对象通过 borrow 方法从 ConcurrentBag 中取出,再通过 requite 方法被放回,有借有还。当线程调用 getConnection 的时候,会调用 ConcurrentBag 的 borrow 方法,它将首先尝试从该线程的 ThreadLocal 最近使用的连接列表中获取未使用的连接。
当关闭连接时,又会通过 remove 方法删除 PoolEntry。ConcurrentBag 就是 HikariCP 的数据库连接存储结构,它在 HikariCP 中起着举足轻重的作用,其性能直接决定 HikariCP 的整体性能。
关闭连接
在可以热部署的 Web 应用程序中,关闭 DataSource 非常重要。通常可以调用 HikariDataSource 实例的 shutdown 或者 close 方法,也可以配置 Spring 或其他 IOC 容器来指定 destroy 方法。一旦 Connection 进入关闭连接阶段,它立即从池中被移除,但是仍然和数据库 DB 有活动连接,直到 Connection.close 完成。HikariCP 永远不会杀死正在使用的连接,除非池本身被关闭。
关闭分为 HikariDataSouce 和 HikariPool 两种关闭,这两者截然不同。
HikariDatasource 作为 HikariCP 对外对使用方提供的 DataSource,它的 close 方法是粗暴的 shutdown 操作。其 close 方法就是直接关闭数据源及其连接池,源码如下所示:
public void close(){
if (isShutdown.getAndSet(true)) {
return;
}
HikariPool p = pool;
if (p ! = null) {
try {
LOGGER.info("{} - Shutdown initiated...", getPoolName());
p.shutdown();
LOGGER.info("{} - Shutdown completed.", getPoolName());
}
catch (InterruptedException e) {
LOGGER.warn("{} - Interrupted during closing", getPoolName(), e);
Thread.currentThread().interrupt();
}
}
}
HikariDataSouce 的 close 直接调用了 HikariPool 的 shutdown 操作,其具体实现在 HikariPool 的 shutdown 方法里,不但关闭连接池,还关闭了所有空闲连接,中止或关闭活动连接。此外,它还会进行直接取消定时任务、关闭 ConcurrentBag 等一系列清理工作,可以认为就是一个 shutdown 的强硬操作。HikariPool 的 shutdown 源码如下所示:
public synchronized void shutdown() throws InterruptedException {
try {
poolState = POOL_SHUTDOWN;
if (addConnectionExecutor == null) { //如果连接池从未启动
return;
}
logPoolState("Before shutdown ");
if (houseKeeperTask ! = null) {//关闭houseKeeper的任务
houseKeeperTask.cancel(false);
houseKeeperTask = null;
}
softEvictConnections(); //软驱逐连接
addConnectionExecutor.shutdown(); //关闭增加连接线程池
addConnectionExecutor.awaitTermination(getLoginTimeout(), SECONDS);
destroyHouseKeepingExecutorService();
connectionBag.close(); //关闭connectionBag
final ExecutorService assassinExecutor = createThreadPoolExecutor(
config.getMaximumPoolSize(),
poolName + " connection assassinator",
config.getThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
try {
final long start = currentTime();
do {
abortActiveConnections(assassinExecutor);
softEvictConnections();
} while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(10));
}
finally {
assassinExecutor.shutdown();
assassinExecutor.awaitTermination(10L, SECONDS);
}
shutdownNetworkTimeoutExecutor();
closeConnectionExecutor.shutdown();
closeConnectionExecutor.awaitTermination(10L, SECONDS);
}
finally {
logPoolState("After shutdown ");
handleMBeans(this, false);
metricsTracker.close();
}
}
与 HikariDataSource 对外提供的 close 接口不同,HikariPool 提供的内部 closeConnection 方法才是我们通常意义上理解的数据库连接池内部的连接关闭逻辑。
当 HikariPool 执行 closeConnection 方法时,首先先从 ConcurrentBag 中移除 PoolEntry,然后 PoolEntry 自身 close,接着独立线程池 closeConnectionExecutor(本质是ThreadPoolExecutor) 调用 JDBC 的方法进行物理连接的关闭。如果 poolState 的状态为 0,还会使用另一个独立线程池 addConnectionExecutor(与 closeConnectionExecutor 对称,本质上也是 ThreadPoolExecutor)进行新连接的生成,fillPool 补足到 HikariCP 的配置值。其核心源码如下所示:
void closeConnection(final PoolEntry poolEntry, final String closureReason)
{
if (connectionBag.remove(poolEntry)) {//ConcurrentBag移除poolEntry
final Connection connection = poolEntry.close(); //poolEntry关闭
closeConnectionExecutor.execute(() -> {
quietlyCloseConnection(connection, closureReason); //jdbc关闭连接
if (poolState == POOL_NORMAL) {
fillPool(); //填充连接池
}
});
}
}
我们可以看到 HikariCP 使用了线程池 closeConnectionExecutor 进行了物理连接的关闭。其实在 HikariCP 的源码里,closeConnectionExecutor 还有一个孪生兄弟 addConnectionExecutor,在 com.zaxxer.hikari.pool.HikariPool 的源码中构造函数初始化的时候就可以看到 public HikariPool(final HikariConfigconfig)
。
private final ThreadPoolExecutor addConnectionExecutor;
private final ThreadPoolExecutor closeConnectionExecutor;
//......
LinkedBlockingQueue<Runnable> addQueue =
new LinkedBlockingQueue<>(config.getMaximumPoolSize()
);
this.addConnectionQueue = unmodifiableCollection(addQueue);
this.addConnectionExecutor = createThreadPoolExecutor(
addQueue,
poolName + "connection adder",
threadFactory,
new ThreadPoolExecutor.DiscardPolicy()
);
this.closeConnectionExecutor = createThreadPoolExecutor(
config.getMaximumPoolSize(),
poolName + " connection closer",
threadFactory, newThreadPoolExecutor.CallerRunsPolicy()
);
addConnectionExecutor 和 closeConnectionExecuto r的初始化就是命中了Java 5 种线程池、4 种拒绝策略、3 种阻塞队列的知识点。addConnectionExecutor 用于创建物理连接,它使用了 DiscardPolicy 策略,默认情况下它就是丢弃被拒绝的任务,实际上就是任务满了就不会抛出异常;而 closeConnectionExecutor 用了 CallerRunsPolicy 策略,如果添加到线程池失败,主线程会直接在 execute 方法的调用线程中运行被拒绝的任务,若执行程序已关闭,则会丢弃该任务。
使用 HikariCP 的用户可能经常会看到类似 connection isevicted or dead 这样的异常,其实这是合理的,关闭死连接对于连接池的资源清理至关重要。
HikariCP 关闭连接的 5 种情况:
- 连接验证失败。这对应用程序是不可见的。连接已停用并已替换。用户会看到一条日志消息:“Failed tovalidate connection…”。
- 连接达到了其 maxLifetime。这对应用程序是不可见的。连接已停用并已替换。用户会看到一个关闭原因:“connection has passed maxLifetime”,或者如果在到达时 maxLifetime 正在使用该连接,用户会晚一点看到原因:“connection is evicted or dead”。
- 用户手动驱逐连接。这对应用程序是不可见的。连接已停用并已替换。用户会看到关闭的原因:“connectionevicted by user”。
- JDBC 调用引发了无法恢复的问题 SQLException。这应该对应用程序可见。用户会看到关闭的原因:“connection is broken”。
生成连接
独立的线程池 addConnectionExecutor 用于创建物理连接,它是在 HikariPool 的构造函数中被初始化的。HikariPool 中的 addConnectionExecutor 在 addBagItem() 和 fillPool() 的时候进行新的物理连接的创建。addBagItem 代表向我们之前介绍的 HikariCP 的数据结构 ConcurrentBag 中放置 Bag 项,它的源码实现其实是在 ConcurrentBag 的 borrrow 也就是连接的借用时执行的。核心代码如下所示:
try {
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
//如果我们已经窃取了另一个等待着的连接,那么请求会新增另一个bag
if (waiting > 1) {
listener.addBagItem(waiting -1);
}
return bagEntry;
}
}
listener.addBagItem(waiting);
在上述代码中,有两处分别执行了 listener.addBagItem(waiting-1)
和 listener.addBagItem(waiting)
,当有并发连接产生时,如果存在等待的连接或者连接已经不可用,则会进行物理连接的创建。
另一个 addConnectionExecutor 在 addBagItem() 来源于 HikariPool 的 fillPool 的方法,它是来源于 HikariPool 内部的单独线程 HouseKeeper,用来将当前的数据库连接池从当前的空闲连接填充到最小空闲连接的指标。
private synchronized void fillPool()
{
final int connectionsToAdd = Math.min(config.getMaximumPoolSize() -
getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
- addConnectionQueue.size();
for (int i = 0; i < connectionsToAdd; i++) {
addConnectionExecutor.submit((i < connectionsToAdd -1) ?
poolEntryCreator : postFillPoolEntryCreator);
}
}
PoolEntry 是一个封装了物理连接的对象,在 HikariPool 中定义了两个 PoolEntry,分别代表我们刚才介绍的连接生成的两种场景(addBagItem 和 fillPool):
private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null
/*logging prefix*/);
private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator
("After adding ");
addConnectionExecutor 线程调用 HikariPool 的 createPoolEntry 方法进行连接生成,HikariPool 继承的父类 PoolBase 提供的 newPoolEntry 会先进行物理连接的创建,创建完成以后的连接会被封装为 PoolEntry 后放入ConcurrentBag。createPoolEntry 的源码如下所示:
private PoolEntry createPoolEntry()
{
try {
final PoolEntry poolEntry = newPoolEntry();
final long maxLifetime = config.getMaxLifetime();
if (maxLifetime > 0) {
// 设置maxlifetime的2.5%的差额
final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.
current().nextLong( maxLifetime / 40 ) : 0; //随机数
final long lifetime = maxLifetime - variance;
poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
() -> {//设置异步任务
if (softEvictConnection(poolEntry, "(connection has passed
maxLifetime)", false /* not owner */)) {
addBagItem(connectionBag.getWaitingThreadCount());
}
},
lifetime, MILLISECONDS));
}
return poolEntry;
}
catch (ConnectionSetupException e) {
if (poolState == POOL_NORMAL) {
//如果shutdown()同时运行,我们检查POOL_NORMAL以避免消息泛滥
logger.error("{} - Error thrown while acquiring connection from data
source", poolName, e.getCause());
lastConnectionFailure.set(e); //设置上一次连接失败的异常
}
return null;
}
HikariCP 的 maxLifetime 默认是 1800000毫秒(30分钟)。在上述代码中,有一段重要的随机数逻辑,这段代码主要就是根据 maxLifetime 的 2.5% 来设置一个差额,如下所示:
// 设置maxlifetime的2.5%的差额
final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().
nextLong( maxLifetime / 40 ) : 0; //随机数
连接生成的时候让每个连接的最大存活时间错开一点,防止同时过期,加一点点随机因素,防止一件事情大量同时发生,比如防止 HikariCP 的连接同时大量死亡。如果 maxLifetime 大于 10000 就是大于 10 秒钟,就执行这个策略,用 maxLifetime 的 2.5% 的时间和 0 之间的随机数来随机设定一个 variance,在 maxLifetime - variance 之后触发 evict。比如,配置 maxLifetime 为 15 分钟时,HikariCP 为每个连接最大寿命注入了 2.5% 的变化,寿命为 15 分钟时,相当于 22.5 秒的变化。一些连接可能在 14 分 38 秒退休,其他连接可能在 14 分 53 秒退休等。
在创建 poolEntry 的时候,注册了一个延时任务,在连接存活将要到达 maxLifetime 之前触发 evit(标记连接池中的连接不可用),用来防止出现大面积连接因为 maxLifetim e是一样的而同时失效,从而造成 HikariCP 数据库连接池不稳定的情况。
在 HikariCP 的 3.3.1 版本中,修复了一个 issue 1287 “setcore pool size before max pool size”,这正是与我们介绍的连接生成 addConnectionExecutor 有关系的。提交这个 issue 的用户反馈:他在执行大量并发耗时查询(查询10~30秒)的时候,启动时的有尖峰请求,在尖峰需求完毕以前池中无空闲连接的情况下反应非常慢,获得新的连接是一个严重阻塞的过程。
根据这个用户的问题,HikariCP 3.3.1 版本在 HikariPool 的初始化过程中统一将 addConnectionExecutor.setMaximumPoolSize 挪到了 addConnectionExecutor.setCorePoolSize 的后面。其源码如下所示:
if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") &&
config.getInitializationFailTimeout() > 1) { addConnectionExecutor.
setCorePoolSize(Runtime.getRuntime().availableProcessors()); addConnectionExecutor.
setMaximumPoolSize(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);
}
com.zaxxer.hikari.blockUntilFilled 是 HikariCP 新版本的一个新功能,它是 HikariCP 官方文档没有提供的系统属性,它的作用是阻塞应用程序直到数据库连接池达到 minimumIdle 值的大小。这个行为在 HikariCP 旧版本期间是单线程的,所以就有可能出现初始化时大量并发连接的长慢查询容易导致 HikariCP 的 getConnection 方法产生阻塞;在 3.3.0 版本中可以进行调整,如果 com.zaxxer.hikari.blockUntilFilled 的属性为 true,并且 initializationFailTimeout 大于 1,则池将由多个线程填充。这线程数由 Runtime.getRuntime().availableProcessors() 确定。初始化后,线程数会在池的生命周期内下降回 1。
那么为什么又要将 setMaximumPoolSize 挪到 setCorePoolSize 后面呢?因为这里有一个 Bug,根据 ThreadPoolExecutor 文档,需要在设置最大池大小之前设置核心池大小,因为在将最大池大小设置为低于核心池大小的值时会抛出 IllegalArgumentException。而这个修复是在 HikariCP 3.3.1 版本中调整的。
setMaximumPoolSize 文档:
public void setMaximumPoolSize(int maximumPoolSize)
Sets the maximum allowed number of threads. This overrides any value set in
the constructor. If the new value is smaller than the current value, excess existing
threads will be terminated when they next become idle.
Parameters:
maximumPoolSize - the new maximum
Throws:
IllegalArgumentException - if the new maximum is less than or equal to zero,
or less than the core pool size
See Also:
getMaximumPoolSize()
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.