【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  小结

好啦,本节就到这里了,下节我们继续。

posted @ 2024-04-22 21:17  酷酷-  阅读(95)  评论(0编辑  收藏  举报