【SpringBoot】【一】初识数据源连接池

1  前言

上节我们看了看,SpringBoot 启动后都有哪些线程,看到有一部分是关于数据源连接池的,那么这节我们就看看数据源连接池都是如何工作的。

我们本节就从这两个问题看起:

(1)数据源是什么时候创建的?连接池是什么时候创建的呢?一起创建的?还是分开创建的?

(2)连接是什么时候放进连接池的?是创建完就初始化了一批新的连接,还是等获取的时候才开始创建?或者说获取连接的过程是什么?

2  实践

2.1  数据源的创建时机

老生常谈,SpringBoot 直接自动装配找起:

进到 DataSourceAutoConfiguration 看看,有两个关键的要素:

(1)DataSourceInitializationConfiguration 数据源初始化配置类(这个就是创建连接池的入口)

(2)5种类型的数据源配置:hikari、tomcat、dbcp、generic、jmx

看到这里,我们思考一个问题,我们可以看到会引入 5种类型的数据源,那么为什么 SpringBoot 默认的数据源是 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;
    }
}

2.2  连接池的创建时机

数据源的创建时机知道了,那么连接池的创建时机是什么时候呢?

这个取决于你的数据源的创建方式,是带参数的还是不带参数的,带参数的会在实例化方法里,就会创建出连接池,不带参数的,会在第一次获取连接的时候,创建连接池,我们看看。

(1)带参数的如下:

public HikariDataSource(HikariConfig configuration) {
   configuration.validate();
   configuration.copyStateTo(this);
   LOGGER.info("{} - Starting...", configuration.getPoolName());
   // 创建连接池
   pool = fastPathPool = new HikariPool(this);
   LOGGER.info("{} - Start completed.", configuration.getPoolName());
   this.seal();
}

(2)不带参数的如下:

public HikariDataSource() {
   super();
   fastPathPool = null;
}
/** {@inheritDoc} */
@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();
}

那么我这个服务启动的时候,它默认采用的哪种呢?是不带参数的,所以连接池的创建是第一次获取连接的时候才创建的。

服务启动后,谁先获取连接的呢?通过看日志以及 debug 会发现:

看启动日志,大家心细的会发现,数据源启动的时候,会有几行日志:

那么谁第一次获取连接的呢?我看线程的方法链路看又是 SpringBoot 的监控,又是 actuator 那么监控包,看 jdbc 连接的健康情况过来的,这玩意看起来还挺重要。

好,那我们第一个问题看下来,首先是自动装配,引入我们的数据源,并且注入了数据源的一个 Processor ,可以用于数据库脚本的启动执行。而数据库连接池的创建是落在第一次获取连接的时候,进行创建的。

2.3  DataSourceInitializationConfiguration

我们还会看见自动装配,会引入一个DataSourceInitializationConfiguration ,那么它的作用是什么呢?我们接着看 DataSourceInitializationConfiguration :

@Configuration(proxyBeanMethods = false)
// 引入了两个类 一个 processor 用于引入后边的 invoker 一个 invoker 用来执行数据库脚本的
@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class })
class DataSourceInitializationConfiguration {
    /**
     * {@link ImportBeanDefinitionRegistrar} to register the
     * {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
     * issues.
     */
    static class Registrar implements ImportBeanDefinitionRegistrar {
        private static final String BEAN_NAME = "dataSourceInitializerPostProcessor";
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                BeanDefinitionRegistry registry) {
            // 注入 Processor
            if (!registry.containsBeanDefinition(BEAN_NAME)) {
                GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class);
                beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                // We don't need this one to be post processed otherwise it can cause a
                // cascade of bean instantiation that we would rather avoid.
                beanDefinition.setSynthetic(true);
                registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
            }
        }
    }
}

那我们继续看看 DataSourceInitializerPostProcessor:

/**
 * {@link BeanPostProcessor} used to ensure that {@link DataSourceInitializer} is
 * initialized as soon as a {@link DataSource} is.
 * Bean 的后置处理器 也就是在 Bean的生命周期最后一步初始化里调用到 后置处理器
 * @author Dave Syer
 */
class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered {
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
    @Autowired
    private BeanFactory beanFactory;
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 如果当前 Bean 是数据源类型的,就把 invoker 拿出来
        if (bean instanceof DataSource) {
            // force initialization of this bean as soon as we see a DataSource
            this.beanFactory.getBean(DataSourceInitializerInvoker.class);
        }
        return bean;
    }
}

那看到这里,我们上边的默认数据源是 Hikari,那么就是在创建这个数据源的时候,进入到这里的 Processor的,是不是这样呢?我们 debug看看,确实如此哈。

那我们继续看看 DataSourceInitializerInvoker:

package org.springframework.boot.autoconfigure.jdbc;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.log.LogMessage;

/**
 * Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on
 * {@link InitializingBean#afterPropertiesSet()} and {@literal data-*.sql} SQL scripts on
 * a {@link DataSourceSchemaCreatedEvent}.
 * 监听事件以及实现了 InitializingBean初始化 Bean 
 * @author Stephane Nicoll
 * @see DataSourceAutoConfiguration
 */
class DataSourceInitializerInvoker implements ApplicationListener<DataSourceSchemaCreatedEvent>, InitializingBean {

    private static final Log logger = LogFactory.getLog(DataSourceInitializerInvoker.class);

    private final ObjectProvider<DataSource> dataSource;

    private final DataSourceProperties properties;

    private final ApplicationContext applicationContext;

    private DataSourceInitializer dataSourceInitializer;

    private boolean initialized;

    DataSourceInitializerInvoker(ObjectProvider<DataSource> dataSource, DataSourceProperties properties,
            ApplicationContext applicationContext) {
        this.dataSource = dataSource;
        this.properties = properties;
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() {
        DataSourceInitializer initializer = getDataSourceInitializer();
        if (initializer != null) {
            boolean schemaCreated = this.dataSourceInitializer.createSchema();
            if (schemaCreated) {
                initialize(initializer);
            }
        }
    }

    private void initialize(DataSourceInitializer initializer) {
        try {
            this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer.getDataSource()));
            // The listener might not be registered yet, so don't rely on it.
            if (!this.initialized) {
                this.dataSourceInitializer.initSchema();
                this.initialized = true;
            }
        }
        catch (IllegalStateException ex) {
            logger.warn(LogMessage.format("Could not send event to complete DataSource initialization (%s)",
                    ex.getMessage()));
        }
    }

    @Override
    public void onApplicationEvent(DataSourceSchemaCreatedEvent event) {
        // NOTE the event can happen more than once and
        // the event datasource is not used here
        DataSourceInitializer initializer = getDataSourceInitializer();
        if (!this.initialized && initializer != null) {
            initializer.initSchema();
            this.initialized = true;
        }
    }

    private DataSourceInitializer getDataSourceInitializer() {
        if (this.dataSourceInitializer == null) {
            DataSource ds = this.dataSource.getIfUnique();
            if (ds != null) {
                this.dataSourceInitializer = new DataSourceInitializer(ds, this.properties, this.applicationContext);
            }
        }
        return this.dataSourceInitializer;
    }

}

我们可以看到该类(1)监听 DataSourceSchemaCreatedEvent 事件 (2)实现了 InitializingBean,那我们刚才在 Processor 里看到的 getBean 也只是创建出来 DataSourceInitializerInvoker 这个对象,由于它是实现了 InitializingBean,所以会执行它的 afterPropertiesSet 方法,那它主要是干什么的呢?

它的作用主要是用来执行数据库脚本的哈。

2.4  连接的获取

不知道大家看过我之前的数据源的连接协同以及在动态数据源下的连接协同,其实连接的获取都是从数据源获取的,那我们这里就直接从 HikariDataSource 的 getConnection 看起:

/** 获取连接 */
@Override
public Connection getConnection() throws SQLException{
   // 已经关闭的话 直接报异常
   if (isClosed()) {
      throw new SQLException("HikariDataSource " + this + " has been closed.");
   }
   // HikariDataSource 的数据源的带参的构造方法中会直接创建连接池并赋值给 fastPathPool 和 pool
   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);
               // 标记 seal 表示已经创建过,防止多次创建
               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)配置以及参数的校验

(2)创建连接池

(3)获取连接

3  小结

好啦,本节就看到这里,关于参数的校验以及连接池的创建,我发现细节还挺多,本节就暂时写到这里,内容太多容易造成疲惫,我们下节再来看这两点哈。本节我们先对连接池的创建时机以及创建过程,有个大概的了解,下节我们继续,有理解错误的地方还请欢迎指正哈。

posted @ 2024-04-21 20:58  酷酷-  阅读(51)  评论(0编辑  收藏  举报