SpringBoot与Hikari数据源分析

1,764 阅读3分钟

1. HikariDataSource

1.1 实例化

DataSourceAutoConfiguration数据源自动配置类中引入了Hikari配置类,声明如下:

@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;
    }

}

配置类中定义了HikariDataSource这样一个Bean,在容器实例化对象的时候调用HikariDataSource构造函数进行初始化

1.2 属性配置

HikariDataSource在声明时使用了@ConfigurationProperties(prefix = "spring.datasource.hikari")注解,会将application.yml配置文件中以spring.datasource.hikari开头的配置注入到该对象中

2.HikariPool

2.1 获取连接入口

DataSourceAutoConfiguration自动配置类中声明了HikariDataSource数据源,Mapper层在执行SQL时会调用SqlSessionFactory持有DataSourcegetConnection()方法,也就是HikariDataSourcegetConnection()方法

2.2 创建HikariPool对象

HikariDataSourcegetConnection()方法会创建连接池对象

public Connection getConnection() throws SQLException {
    HikariPool result = pool;
    if (result == null) {
        synchronized (this) {
            result = pool;
            if (result == null) {
				pool = result = new HikariPool(this);
            }
        }
    }
    return result.getConnection();
}

2.1 创建DriverDataSource对象

根据配置文件中url用户名密码创建DriverDataSource对象

private void initializeDataSource() {
    final String jdbcUrl = config.getJdbcUrl();
    final String username = config.getUsername();
    final String password = config.getPassword();
    final String dsClassName = config.getDataSourceClassName();
    final String driverClassName = config.getDriverClassName();
    final String dataSourceJNDI = config.getDataSourceJNDI();
    final Properties dataSourceProperties = config.getDataSourceProperties();

    DataSource ds = config.getDataSource();
    if (dsClassName != null && ds == null) {
        ds = createInstance(dsClassName, DataSource.class);
        PropertyElf.setTargetFromProperties(ds, dataSourceProperties);
    }
    else if (jdbcUrl != null && ds == null) {
        ds = new DriverDataSource(jdbcUrl, driverClassName, dataSourceProperties, username, password);
    }

    this.dataSource = ds;
}

2.2 初始化连接

如果配置了初始化失败超时时间,则会创建数据库连接

private void checkFailFast() {
    // 获取初始化失败超时时间
    final long initializationTimeout = config.getInitializationFailTimeout();
    if (initializationTimeout < 0) {
        return;
    }

    do {
        final PoolEntry poolEntry = createPoolEntry();
    } while (elapsedMillis(startTime) < initializationTimeout);

    if (initializationTimeout > 0) {
        throwPoolInitializationException(getLastConnectionFailure());
    }
}

2.2.1 DriverDataSource创建数据库连接

private Connection newConnection() throws Exception {
    Connection connection = null;
    try {
        String username = config.getUsername();
        String password = config.getPassword();

        connection = (username == null) ? dataSource.getConnection() : dataSource.getConnection(username, password);
        if (connection == null) {
            throw new SQLTransientConnectionException("DataSource returned null unexpectedly");
        }
        return connection;
    } catch (Exception e) {}
    finally {}
}

2.2.2 构建PoolEntry

根据创建的数据库连接来构建PoolEntry

PoolEntry newPoolEntry() throws Exception{
    return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
}

2.2.3 存储数据库连接

connectionBag.add(poolEntry);

将数据库连接放入集合集合列表中

public void add(final T bagEntry){
    sharedList.add(bagEntry);
}

2.3 填充连接池

2.3.1 创建调度任务

在创建HikariPool时会定义一个调度任务用来创建minimum-idle数量的数据库连接以此来填充连接池

this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

2.3.2 提交创建数据连接任务

假设minimum-idlemaximum-pool-size都为10,则此处需要创建的连接数为9,也就是会提交9个任务用来创建数据库连接,创建连接逻辑与2.2章节一致

private synchronized void fillPool(){
    final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections()) - addConnectionQueueReadOnlyView.size();
    if (connectionsToAdd <= 0) logger.debug("{} - Fill pool skipped, pool is at sufficient level.", poolName);

    for (int i = 0; i < connectionsToAdd; i++) {
        addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
    }
}

3. 获取连接

3.1 借用连接

获取数据库连接先调用HikariDataSourcegetConnection()方法,然后调用连接池HikariPoolgetConnection(),最后从池中借用连接

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
    // 1. 先从线程上下文中获取连接,如果存在并且处于"空闲"状态,直接返回
    // 此方式可以避免锁竞争(在Mapper层多几次与数据库的交互,就可以体现出该逻辑的优势)
    final List<Object> list = threadList.get();
    for (int i = list.size() - 1; i >= 0; i--) {
        final Object entry = list.remove(i);
        @SuppressWarnings("unchecked")
        final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
        if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
        }
    }

    // 2. 在2.2章节部分创建的数据库连接最终都会存放到sharedList中,此处会遍历sharedList,如果连接处于"空闲"状态,直接返回
    final int waiting = waiters.incrementAndGet();
    try {
        for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                // If we may have stolen another waiter's connection, request another bag add.
                if (waiting > 1) {
                    listener.addBagItem(waiting - 1);
                }
                return bagEntry;
            }
        }

        // 3. 符合创建数据库连接条件,则创建连接并放入队列中
        listener.addBagItem(waiting);

        timeout = timeUnit.toNanos(timeout);
        do {
            final long start = currentTime();
            // 从队列中取出数据库连接
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
                return bagEntry;
            }

            timeout -= elapsedNanos(start);
        } while (timeout > 10_000);

        return null;
    }
    finally {
        waiters.decrementAndGet();
    }
}

3.2 超时处理

connection-timeout时间内未获取到数据库连接,则会抛出SQLException异常

private SQLException createTimeoutException(long startTime) {
    logPoolState("Timeout failure ");
    metricsTracker.recordConnectionTimeout();

    String sqlState = null;
    final Throwable originalException = getLastConnectionFailure();
    if (originalException instanceof SQLException) {
        sqlState = ((SQLException) originalException).getSQLState();
    }
    final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, originalException);
    if (originalException instanceof SQLException) {
        connectionException.setNextException((SQLException) originalException);
    }

    return connectionException;
}

4. 回收连接

public void requite(final T bagEntry){
    // 1. 将连接状态设置为"空闲"状态
    bagEntry.setState(STATE_NOT_IN_USE);

    // 2. 存在排队情况,则将"空闲"连接放入队列(3.1章节借用连接的第三个场景会从队列中获取连接)
    for (int i = 0; waiters.get() > 0; i++) {
        if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
        }
        else if ((i & 0xff) == 0xff) {
            parkNanos(MICROSECONDS.toNanos(10));
        }
        else {
            Thread.yield();
        }
    }

    // 3.将"空闲"连接放入线程上下文中(3.1章节借用连接的第一个场景会从线程上下文中获取连接)
    final List<Object> threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) {
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
    }
}