SpringBoot HikariCP配置项及源码解析
作者:MrDong先生
前言
在SpringBoot2.0之后,采用的默认数据库连接池就是Hikari,是一款非常强大,高效,并且号称“史上最快连接池”。我们知道的连接池有C3P0,DBCP,Druid它们都比较成熟稳定,但性能不是十分好。
我们在日常的编码中,通常会将一些对象保存起来,这主要考虑的是对象的创建成本;比如像线程资源、数据库连接资源或者 TCP 连接等,这类对象的初始化通常要花费比较长的时间,如果频繁地申请和销毁,就会耗费大量的系统资源,造成不必要的性能损失,于是在Java 中,池化技术应用非常广泛。在软件行开发中,软件的性能是占主导地位的,于是HikariCP就在众多数据库连接池中脱颖而出。
为什么HikariCP性能高
- 优化代理和拦截器:减少代码。
- 字节码精简 :优化代码(
HikariCP
利用了一个第三方的Java字节码修改类库Javassist
来生成委托实现动态代理,动态代理的实现在ProxyFactor
y类),直到编译后的字节码最少,这样,CPU
缓存可以加载更多的程序代码。 - 通过代码设计和优化大幅减少线程间的锁竞争。这点主要通过
ConcurrentBag
来实现。 - 自定义数组类型(
FastStatementList
)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描,相对与ArrayList
极大地提升了性能,而其中的区别是,ArrayList
在每次执行get(Index)
方法时,都需要对List
的范围进行检查,而FastStatementList
不需要,在能确保范围的合法性的情况下,可以省去范围检查的开销。自定义集合类型(ConcurrentBag
):支持快速插入和删除,特别是在同一线程既添加又删除项时,提高并发读写的效率; - 关于
Connection的
操作:另外在Java
代码中,很多都是在使用完之后直接关闭连接,以前都是从头到尾遍历,来关闭对应的Connection
,而HikariCP
则是从尾部对Connection
集合进行扫描,整体上来说,从尾部开始的性能更好一些。 - 针对连接中断的情况:比其他CP响应时间上有了极好的优化,响应时间为5S,会抛出
SqlException
异常,并且后续的getConnection()可以正常进行
下面为大家附上一张官方的性能测试图,我们可以从图上很直观的看出HikariCP的性能卓越:
常用配置项
autoCommit
控制从池返回的连接的默认自动提交行为,默认为true
connectionTimeout
控制客户端等待来自池的连接的最大毫秒数。
如果在没有连接可用的情况下超过此时间,则将抛出 SQLException
。可接受的最低连接超时时间为 250 毫秒
。默认值:30000(30 秒)
idleTimeout
连接允许在池中闲置的最长时间
如果idleTimeout+1秒>maxLifetime
且 maxLifetime>0,则会被重置为0(代表永远不会退出);如果idleTimeout!=0且小于10秒,则会被重置为10秒
这是HikariCP
用来判断是否应该从连接池移除空闲连接的一个重要的配置。负责剔除的也还是HouseKeeper
这个定时任务,值为0时,HouseKeeper
不会移除空闲连接,直到到达maxLifetime
后,才会移除,默认值也就是0。
正常情况下,HouseKeeper
会找到所有状态为空闲的连接队列,遍历一遍,将空闲超时到达idleTimeout
且未超过minimumIdle
数量的连接的批量移除。
maxLifetime
池中连接最长生命周期;如果不等于0且小于30秒则会被重置回30分钟
了解这个值的作用前,先了解一下MySQLwait_timeout
的作用:MySQL 为了防止空闲连接浪费,占用资源,在超过wait_timeout
时间后,会主动关闭该连接,清理资源;默认是28800s
,也就是8小时。简而言之就是MySQL会在某个连接超过8小时还没有任何请求时自动断开连接,但是HikariCP如何知道池子里的连接有没有超过这个时间呢?所以就有了maxLifetime
,配置后HikariCP会把空闲链接超过这个时间的给剔除掉,防止获取到已经关闭的连接导致异常。
connectionTestQuery
将在从池中向您提供连接之前执行的查询,以验证与数据库的连接是否仍然有效,如select 1
minimumIdle
池中维护的最小空闲连接数;minIdle<0或者minIdle>maxPoolSize,则被重置为maxPoolSize
在HikariCP Pool
创建时,会启动一个HouseKeeper
定时任务,每隔30s,判断空闲线程数低于minimumIdle
,并且当前线程池总连接数小于maximumPoolSize
,就建立和MySQL的一个长连接,然后加入到连接池中。官方建议minimumIdle
和maximumPoolSize
保持一致。 因为HikariCP
的HouseKeeper
在发现idleTimeout>0 并且 minimumIdle < maximumPoolSize时,先会去扫描一遍需要移除空闲连接,和MySQL断开连接。然后再一次性补满空闲连接数至到minimumIdle
。
maximumPoolSize
池中最大连接数,其实就是线程池中队列的大小,默认大小为10(包括闲置和使用中的连接)
如果maxPoolSize
小于1,则会被重置。当minIdle<=0被重置为DEFAULT_POOL_SIZE
则为10;如果minIdle>0则重置为minIdle
的值
HikariCP架构
分析源码之前,先给大家介绍一下HikariCP的整体架构,整体架构和DBCP2 的有点类似(由此可见 HikariCP 与 DBCP2 性能差异并不是由于架构设计),下面我总结了几点,来和大家一起探讨下:
HikariCP
通过JMX
调用HikariPoolMXBean
来获取连接池的连接数、获取等待连接的线程数、丢弃未使用连接、挂起和恢复连接池等。HikariCP
通过JMX
调用HikariConfigMXBean
来动态修改配置。HikariCP
使用HikariConfig
加载配置文件,一般会作为入参来构造 HikariDataSource 对象。HikariPool
是一个非常重要的类,它负责管理连接,涉及到比较多的代码逻辑。HikariDataSource
主要用于操作HikariPool
获取连接。ConcurrentBag
用于优化大幅减少线程间的锁竞争。PoolBase
是HikariPool
的父类,主要负责操作实际的DataSource
获取连接,并设置连接的一些属性。
源码解析
HikariConfig
HikariConfig
保存了所有连接池配置,另外实现了HikariConfigMXBean
接口,有些配置可以利用JMX
运行时变更。核心配置项属性会在下面给大家介绍,这边Dong哥就简单介绍一下了。
HikariPool
getConnection
public Connection getConnection(final long hardTimeout) throws SQLException { //这里是防止线程池处于暂停状态(通常不允许线程池可暂停) suspendResumeLock.acquire(); final long startTime = currentTime(); try { long timeout = hardTimeout; do { //PoolEntry 用于跟踪connection实例,里面包装了Connection; //从connectionBag中获取一个对象,并且检测是否可用 PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS); if (poolEntry == null) { break; // We timed out... break and throw exception } final long now = currentTime(); //1、已被标记为驱逐 2、已超过最大存活时间 3、链接已死 if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) { closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); //刷新超时时间 timeout = hardTimeout - elapsedMillis(startTime); } else { metricsTracker.recordBorrowStats(poolEntry, startTime); return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now); } //如果没超时则再次获取 } while (timeout > 0L); //超时时间到仍未获取到链接则抛出 TimeoutException metricsTracker.recordBorrowTimeoutStats(startTime); throw createTimeoutException(startTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(poolName + " - Interrupted during connection acquisition", e); } finally { suspendResumeLock.release(); } }
校验:
- isMarkedEvicted:检查当前链接是否已被驱逐
- elapsedMillis(poolEntry.lastAccessed, now):检查链接是否超过最大存活时间(
maxLifetime
配置时间)
/** * startTime 上次使用时间 * endTime 当前时间 */ static long elapsedMillis(long startTime, long endTime) { return CLOCK.elapsedMillis0(startTime, endTime); }
- isConnectionAlive:连接是否还是存活状态
boolean isConnectionAlive(final Connection connection) { try { try { //如果支持Connection networkTimeout,则优先使用并设置 setNetworkTimeout(connection, validationTimeout); final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000; //如果jdbc实现支持jdbc4 则使用jdbc4 Connection的isValid方法检测 if (isUseJdbc4Validation) { return connection.isValid(validationSeconds); } //查询数据库检测连接可用性 try (Statement statement = connection.createStatement()) { //如果不支持Connection networkTimeout 则设置Statement queryTimeout 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 {} ({}). Possibly consider using a shorter maxLifetime value.", poolName, connection, e.getMessage()); //捕获到异常,说明链接不可用。(connection is unavailable) return false; } }
HouseKeeper
HouseKeeper
负责保持,我们始终有minimumIdle空闲链接可用
private final class HouseKeeper implements Runnable { //默认30s,执行一次 private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS); @Override public void run() { try { //省略...... String afterPrefix = "Pool "; if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) { logPoolState("Before cleanup "); afterPrefix = "After cleanup "; //空闲链接数 final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE); int toRemove = notInUse.size() - config.getMinimumIdle(); for (PoolEntry entry : notInUse) { if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) { //关闭过多的空闲超时链接 closeConnection(entry, "(connection has passed idleTimeout)"); toRemove--; } } } //记录pool状态信息 logPoolState(afterPrefix); //补充空闲链接 fillPool(); } catch (Exception e) { LOGGER.error("Unexpected exception in housekeeping task", e); } } }
HouseKeeper
其实是一个线程,也是写在HikariPool
类里面的一个内部类,主要负责保持 minimumIdle
的空闲链接。HouseKeeper
也用到了validationTimeout
, 并且会根据minimumIdle
配置,通过fill 或者 remove保持最少空闲链接数。HouseKeeper
线程初始化:
public HikariPool(final HikariConfig config) { super(config); this.connectionBag = new ConcurrentBag<>(this); this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK; //执行初始化 this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); //省略...... } }
private ScheduledExecutorService initializeHouseKeepingExecutorService() { if (this.config.getScheduledExecutor() == null) { ThreadFactory threadFactory = (ThreadFactory)Optional.ofNullable(this.config.getThreadFactory()).orElseGet(() -> { return new DefaultThreadFactory(this.poolName + " housekeeper", true); }); //ScheduledThreadPoolExecutor是ThreadPoolExecutor类的子类,Java推荐仅在开发定时任务程序时采用ScheduledThreadPoolExecutor类 ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new DiscardPolicy()); //传入false,则执行shutdown()方法之后,待处理的任务将不会被执行 executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); //取消任务后,判断是否需要从阻塞队列中移除任务 executor.setRemoveOnCancelPolicy(true); return executor; } else { return this.config.getScheduledExecutor(); } }
HikariDataSource
HikariDataSource
非常重要,主要用于操作HikariPool
获取连接,并且能够清除空闲连接。
public class HikariDataSource extends HikariConfig implements DataSource, Closeable { private final AtomicBoolean isShutdown = new AtomicBoolean(); //final修饰,构造时决定,如果使用无参构造为null,使用有参构造和pool一样 private final HikariPool fastPathPool; //volatile修饰,无参构造不会设置pool,在getConnection时构造pool,有参构造和fastPathPool一样。 private volatile HikariPool pool; public HikariDataSource() { super(); fastPathPool = null; } public HikariDataSource(HikariConfig configuration) { configuration.validate(); configuration.copyStateTo(this); pool = fastPathPool = new HikariPool(this); this.seal(); } }
以上就是SpringBoot HikariCP配置项及源码解析的详细内容,更多关于SpringBoot HikariCP配置的资料请关注脚本之家其它相关文章!