【SpringBoot】【二】初识数据源连接池
1 前言
上节我们从整体上看了下数据源连接池的创建入口,以及连接池创建的时机和获取连接的过程,对于连接池的创建我们只是粗糙的看了下,那么这节我们就详细看一下 HikariDataSource 数据源的创建过程,以及连接池的创建过程。
2 实践
2.1 数据源的创建过程
那我们就还是从 DataSourceAutoConfiguration 看起:
// 数据源自动装配类 @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) @Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { // 引入五种类型的数据源 @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } // hikari数据源 /** * Hikari DataSource configuration. */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") HikariDataSource dataSource(DataSourceProperties properties) { // 创建数据源 HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } } }
我们继续看 DataSourceConfiguration 的 createDataSource 方法:
/** * @param properties DataSourceProperties 数据源配置 * @param type 这里就是我们的 HikariDataSource 数据源 * @param <T> 泛型方法 返回的也就是 HikariDataSource 数据源对象 * @return */ @SuppressWarnings("unchecked") protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) { // 建造者模式 return (T) properties // 基础信息检查 数据源类、url、账号、密码 .initializeDataSourceBuilder() // 数据源 .type(type) // 构建对象 .build(); }
initializeDataSourceBuilder 方法里比较简单,主要是对驱动、url、账号、密码的检查赋值,我们直接看 build 方法:
@link DataSourceBuilder public T build() { // 获取数据源类型 比如我们这里的 HikariDataSource Class<? extends DataSource> type = getType(); // 反射实例化数据源对象 DataSource result = BeanUtils.instantiateClass(type); // 当没有指定 driverClassName 的情况下,根据 url 进行驱动的一个推断 比如是mysql或者pgsql啥的 maybeGetDriverClassName(); // 数据源信息的绑定 bind(result); // 返回结果 return (T) result; }
build 方法里,主要通过反射的方式将 HikariDataSource 对象实例化出来了。
那么还有最后一点,就是我们的 spring.datasource.hikari 属性配置是什么时候,设置进去 HikariDataSource 对象的。
我们再回到最初的 @Bean 这里看看:
@Configuration @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { // 答案在这里 @Bean + @ConfigurationProperties @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") public HikariDataSource dataSource(DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
ConfigurationPropertiesBindingPostProcessor 后置处理器,在 Bean 生命周期的 initializeBean 里通过 postProcessBeforeInitialization 将属性赋值给该Bean。
最后我们小小的画个图捋一下:
2.2 连接池的创建过程
看完数据源的实例化,我们接下来看看连接池的创建,上节我们看到是第一次获取连接的时候创建的,我们就从获取连接的方法看起:
/** HikariDataSource */ @Override public Connection getConnection() throws SQLException { if (isClosed()) { throw new SQLException("HikariDataSource " + this + " has been closed."); } if (fastPathPool != null) { return fastPathPool.getConnection(); } HikariPool result = pool; if (result == null) { synchronized (this) { result = pool; if (result == null) { // 校验 validate(); LOGGER.info("{} - Starting...", getPoolName()); 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()); } } } // 从连接池中获取连接 return result.getConnection(); }
我们主要看两个:
(1)validate 校验
(2)new HikariPool(this) 创建连接池
2.2.1 validate 校验
我们看一下 HikariDataSource 的 validate 方法,因为 HikariDataSource 继承了 HikariConfig,所以这里的校验实际上是调用的 HikariConfig 的 validate 方法如下:
// HikariDataSource 继承了 HikariConfig // HikariConfig#validate public void validate() { if (poolName == null) { // 当连接池名称为空时,生成名字 poolName = generatePoolName(); } // 这个不太清楚缘由 JMX是种测试 当 JMX测试的时候,连接池名字不能包含 : else if (isRegisterMbeans && poolName.contains(":")) { throw new IllegalArgumentException("poolName cannot contain ':' when used with JMX"); } // 基础去重 是空的话就是空 不是空的话会去除前后空白 catalog = getNullIfEmpty(catalog); connectionInitSql = getNullIfEmpty(connectionInitSql); connectionTestQuery = getNullIfEmpty(connectionTestQuery); transactionIsolationName = getNullIfEmpty(transactionIsolationName); dataSourceClassName = getNullIfEmpty(dataSourceClassName); dataSourceJndiName = getNullIfEmpty(dataSourceJndiName); driverClassName = getNullIfEmpty(driverClassName); jdbcUrl = getNullIfEmpty(jdbcUrl); // Check Data Source Options if (dataSource != null) { if (dataSourceClassName != null) { LOGGER.warn("{} - using dataSource and ignoring dataSourceClassName.", poolName); } } else if (dataSourceClassName != null) { if (driverClassName != null) { LOGGER.error("{} - cannot use driverClassName and dataSourceClassName together.", poolName); // NOTE: This exception text is referenced by a Spring Boot FailureAnalyzer, it should not be // changed without first notifying the Spring Boot developers. throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together."); } else if (jdbcUrl != null) { LOGGER.warn("{} - using dataSourceClassName and ignoring jdbcUrl.", poolName); } } else if (jdbcUrl != null || dataSourceJndiName != null) { // ok } else if (driverClassName != null) { LOGGER.error("{} - jdbcUrl is required with driverClassName.", poolName); throw new IllegalArgumentException("jdbcUrl is required with driverClassName."); } else { LOGGER.error("{} - dataSource or dataSourceClassName or jdbcUrl is required.", poolName); throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required."); } validateNumerics(); if (LOGGER.isDebugEnabled() || unitTest) { logConfiguration(); } }
可以看到都是一些参数的基础检查校验哈,我们这里看看连接池的默认名字生成方法 generatePoolName:
private String generatePoolName() { // 默认的前缀 final String prefix = "HikariPool-"; try { // Pool number is global to the VM to avoid overlapping pool numbers in classloader scoped environments synchronized (System.getProperties()) { // 数值 final String next = String.valueOf(Integer.getInteger("com.zaxxer.hikari.pool_number", 0) + 1); System.setProperty("com.zaxxer.hikari.pool_number", next); return prefix + next; } } catch (AccessControlException e) { ... } }
2.2.2 new HikariPool(this) 创建连接池
接下来得看看重头戏,这个大家伙:
/** * Construct a HikariPool with the specified configuration. * 连接池创建 * @param config a HikariConfig instance */ public HikariPool(final HikariConfig config) { super(config); // 构建一个connectionBag用于保存连接, connectionBag是连接池的核心 this.connectionBag = new ConcurrentBag<>(this); // 根据是否允许挂起连接池, 初始化锁 this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK; this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); checkFailFast(); // 连接池统计 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); if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) { addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors())); addConnectionExecutor.setMaximumPoolSize(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); } }
可以看到东西蛮多的,我们从这个先简单了解下:
(1)ConcurrentBag 是整个 HikariCP 的核心,初始化了用于保存数据库连接的容器,它的内部是一个 CopyOnWriteArrayList
,用于保存连接,这个家伙不是一丁半点能说明白的,这个我们先简单了解,后续要单独讲哈。
(2)checkFailFast 快速失败:目的就是在启动期间,创建连接来验证关键参数是否有错误,如果不能建立连接,立即抛出错误,方便用户及时发现问题。
(3)初始化连接池:
closeConnectionExecutor :用于执行关闭底层连接的线程池,只有一个线程,线程任务队列最大是连接池最大连接数,超出队列的任务,会不断重试添加。
addConnectionExecutor:用于执行添加新连接的线程池,只有一个线程,线程任务队列最大是连接池最大连接数,超出队列的任务,直接抛弃。
houseKeepingExecutorService:这是一个定时线程池,默认只有一个线程,它的作用比较多:用于执行检测连接泄露、关闭空闲时间超期的连接、回收空闲连接、检测时间回拨。
closeConnectionExecutor
的队列任务抛弃策略有点不一样,它会不断重试,是基于连接必须关闭的考虑,其他的任务直接抛弃是影响不大。
这里有两项配置可以影响线程池,一个是scheduledExecutor
:用于提供给houseKeepingExecutorService
用的线程池,如果用户不自定义,就使用默认的 1 个线程的线程池。另一个是threadFactory
:用于生成线程池中的线程,HikariCP 会在生成线程池的时候,调用该线程工厂获取线程。
(4)启动连接管理任务:houseKeepingExecutorService 线程池里提交了一个任务:每隔 30 秒,就执行一次HouseKeeper
任务。这个任务的功能主要是:检测时间回拨,调整连接池里的连接。比如用户修改了服务器的系统时间,因为 HikariCP 是对时间敏感的框架,它靠定时任务来管理连接,如果系统时间变了,那么定时任务就不准确了。当用户调快了时间,这个时候,HikariCP 什么都不做,因为时间快了,只是加快了定时任务的执行,使连接更早过期,这个对连接池影响不大,因为连接池会自动添加新连接。当用户调慢了时间,也就是回退了时间。回退时间对 HikariCP 是有极大影响的,比如原来还差 1 秒执行的任务,现在可能要过 15秒之后才能执行了,这可能引发本来该存活时间到期的连接,不会过期了。所以,这个时候,HikariCP 会把连接池中所以的连接都软驱逐掉,使所有的连接都不可用,然后重新创建新连接。
(5)连接泄露检测任务的父任务:ProxyLeakTask 用户获取到每个连接的时候,都会为该连接创建一个连接泄露检测的定时任务,在指定的时间内,抛出连接泄露警告。在创建连接泄露检测任务的时候,会使用一个父任务的参数,从这个父任务中拿连接泄露的最大时间和用于执行任务的线程池,然后使用这两个参数创建任务。这个父任务,就是在这里创建的,创建的时候就是传了这两个参数:连接泄露的最大时间和用于执行任务的线程池。
3 小结
好啦,本节就到这里了,下节我们继续。