前言
在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配置的资料请关注其它相关文章!