多读书多实践,勤思考善领悟

史上最快数据源连接池HikariCP

本文于1939天之前发表,文中内容可能已经过时。

介绍

官网地址: https://github.com/brettwooldridge/HikariCP
现在介绍一款非常强大,高效,并且号称“史上最快连接池”,它是一个高速、免费、开源的JAVA连接池,它的性能几乎是C3P0、DBCP的25倍,十分强悍。spring boot2.0已经将HikariCP做为了默认的数据源链接池,在官网测试中秒杀一切其他数据源,比如commons-dbcp,tomcat,c3po,druid。

入图片描述

基本设计

Hikari链接池采用了很多优化来提高并发数,可参考这里
所有数据库链接池都遵守基本的设计规则,实现 javax.sql.DataSource 接口,里面最重要的方法就是 Connection getConnection() throws SQLException; 用于获取一个Connection, 一个Connection就是一个数据库链接,就是一个TCP链接,建立TCP链接是需要进行3次握手的,这降低来链接的使用效率,也是各种数据库链接池存在的原因。

数据库链接池通过事先建立好Connection并缓存起来,这样应用需要做数据查询的时候,直接从缓存中拿到Connection就可以使用来。数据库链接池还能够检测异常的链接,释放闲置的链接。

HikariDataSource

Hikari中提供的DataSource是HikariDataSource ,HikariDataSource 实现了 HikariConfig,和数据库的各种参数超时时间配置就正HikariaConfig中。

其中提供两种初始化方式,一种是默认的构造函数,单new一个HikariDataSource时,数据源的链接不会建立,需要等到第一次调用HikariDataSource的getConnection方法。数据源建立后的相关信息保存在 HikariDataSource 中变量HikariPool pool。

另一种建立方式是调用带有HikariConfig的构造函数,这种方式适合多个数据源的建立,共享同一份配置。 这种方式在调用构造函数的时候就建立了数据源的链接。

HikariDataSource的所有数据源获取都委托给了HikariPool,一个数据源会有一个HikariPool,一个HikariPool中有一个ConcurrentBag,一个ConcurrentBag中多个PoolEntry,一个PoolEntry对应一个Connection。

HikariPool

HikariPool中的基本field介绍。

PoolEntryCreator POOL_ENTRY_CREATOR:用于创建PoolEntry,也就是用于创建Connection了,创建Connection是委托给驱动程序的。

Collection addConnectionQueue :就是一个LinkedBlockingQueue,不过不能修改其中的内容。当正在创建底层Connection的时候这个Queue会有值。用户后续判断当前线程池里面还是否需要创建新的链接。

ThreadPoolExecutor addConnectionExecutor:创建Connection链接的执行是有这个线程池调度的。使用新的链接池不会而不使用当前的工作线程,为了不影响工作线程的执行(比如会导致工作线程超时)。

ThreadPoolExecutor closeConnectionExecutor:关闭Connection的链接是有这个线程池调度的。

ConcurrentBag connectionBag:这个是最重要的,每次获取Connection都是从这里面获取,采用了ThreadLocal来减少竞争。

ProxyLeakTaskFactory leakTaskFactory;参考ProxyLeakTask用于检测Connection泄漏。

ScheduledFuture<?> houseKeeperTask:用于管理链接池里面的链接,比如链接不够用了要创建链接,链接最大生存时间到了要关闭链接。线程池中的Connection就是有它初始化的。

ConcurrentBag

ConcurrentBag设计主要为了解决并发对资源的争用,其中主要有CopyOnWriteArrayList sharedList和ThreadLocal<List> threadList,SynchronousQueue handoffQueue,当一个线程获取链接的时候首先从自己的ThreadLocal中的threadList获取,当获取失败的时候才从sharedList获取,当从sharedList获取还是失败的话,就等待在 handoffQueue,这是一个同步的Queue,当其他线程释放链接的时候,自己就会被唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// 从threadList获取链接
final List<Object> list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}

// 从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;
}
}
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final long start = currentTime();
//等待其他线程释放链接,超过timeout就获取失败了
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();
}
}

详细配置

原版英文详细配置查看这里

springboot的HikariDataSource默认配置的默认值如下

name 构造器默认值 默认配置validate之后的值 validate重置
minIdle -1 10 minIdle<0或者minIdle>maxPoolSize,则被重置为maxPoolSize
maxPoolSize -1 10 如果maxPoolSize小于1,则会被重置。当minIdle<=0被重置为DEFAULT_POOL_SIZE则为10;如果minIdle>0则重置为minIdle的值
maxLifetime MINUTES.toMillis(30) = 1800000 1800000 如果不等于0且小于30秒则会被重置回30分钟
connectionTimeout SECONDS.toMillis(30) = 30000 30000 如果小于250毫秒,则被重置回30秒
validationTimeout SECONDS.toMillis(5) = 5000 5000 如果小于250毫秒,则会被重置回5秒
loginTimeout 10 30 Math.max(1, (int) MILLISECONDS.toSeconds(500L + connectionTimeout)),为connectionTimeout+500ms转为秒数取整 与 1 取最大者
idleTimeout MINUTES.toMillis(10) = 600000 600000 如果idleTimeout+1秒>maxLifetime 且 maxLifetime>0,则会被重置为0;如果idleTimeout!=0且小于10秒,则会被重置为10秒
leakDetectionThreshold 0 0 如果大于0且不是单元测试,则进一步判断:(leakDetectionThreshold < SECONDS.toMillis(2) or (leakDetectionThreshold > maxLifetime && maxLifetime > 0),会被重置为0 . 即如果要生效则必须>0,而且不能小于2秒,而且当maxLifetime > 0时不能大于maxLifetime
initializationFailTimeout 1 1 -
isAutoCommit true true -
isReadOnly false fasle -
isAllowPoolSuspension false false -
isIsolateInternalQueries false false -
isRegisterMbeans false false -
sealed false true 运行启动后这个标志为true,表示不再运行修改
poolName null HikariPool-1 -
catalog null null -
connectionInitSql null null -
connectionTestQuery null null -
dataSourceClassName null null -
schema null null -
transactionIsolationName null null -
dataSource null null -
dataSourceProperties {} {} -
threadFactory null null -
scheduledExecutor null null -
metricsTrackerFactory null null -
metricRegistry null null -
healthCheckRegistry null null -
healthCheckProperties {} {} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
## 数据库配置
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username = root
spring.datasource.password = root
## Hikari 连接池配置 ------ 详细配置请访问:https://github.com/brettwooldridge/HikariCP
## 最小空闲连接数量
spring.datasource.hikari.minimum-idle=5
## 空闲连接存活最大时间,默认600000(10分钟)
spring.datasource.hikari.idle-timeout=180000
## 连接池最大连接数,默认是10
spring.datasource.hikari.maximum-pool-size=10
## 此属性控制从池返回的连接的默认自动提交行为,默认值:true
spring.datasource.hikari.auto-commit=true
## 连接池母子
spring.datasource.hikari.pool-name=MyHikariCP
## 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
spring.datasource.hikari.max-lifetime=1800000
## 数据库连接超时时间,默认30秒,即30000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1

配置多数据源

application.yml :

1
2
3
4
5
6
7
8
9
hikari:
primary:
jdbc-url: jdbc:mysql://localhost:3306/xu?useSSL=false
username: root
password: 1234
second:
jdbc-url: jdbc:mysql://localhost:3306/hua?useSSL=false
username: root
password: 1234

PrimaryDatasourceConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
@MapperScan(basePackages = PrimaryDatasourceConfig.PACKAGE,sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDatasourceConfig {

static final String PACKAGE = "com.xu.scaffold.repository.primary";

@Bean(name = "primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "hikari.primary")
public HikariDataSource dataSource() {
return new HikariDataSource();
}

@Bean(name = "primaryTransactionManager")
@Primary
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(this.dataSource());
}

@Bean(name = "primarySqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sessionFactory.getObject();
}
}

SecondDatasourceConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@MapperScan(basePackages = SecondDatasourceConfig.PACKAGE, sqlSessionFactoryRef = "secondSqlSessionFactory")
public class SecondDatasourceConfig {

static final String PACKAGE = "com.xu.scaffold.repository.second";

@Bean(name = "secondDataSource")
@ConfigurationProperties(prefix = "hikari.second")
public HikariDataSource dataSource() {
return new HikariDataSource();
}

@Bean(name = "secondTransactionManager")
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(this.dataSource());
}

@Bean(name = "secondSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("secondDataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sessionFactory.getObject();
}
}