引言
Springboot 2.0将
HikariCP
作为默认数据库连接池这一事件之后,HikariCP 作为一个后起之秀出现在大众的视野中。HikariCP 是在日本的程序员开源的,hikari日语意思为“光”,HikariCP 也以速度快的特点受到越来越多人的青睐。
今天就让我们来探讨一下HikariCP为什么这么快?
连接池技术
我们平常编码过程中,经常会碰到线程池啊,数据库连接池啊等等,那么这个池到底是一门怎样的技术呢?
简单来说,
连接池是一个创建和管理连接的缓冲池技术
。连接池主要由三部分组成:连接池的建立、连接池中连接的使用管理、连接池的关闭。
连接池技术的核心思想是:
连接复用
,通过建立一个数据库连接池以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用。它不仅仅只限于管理数据库访问连接,也可以管理其他连接资源。
HakariCP
HakariCP 项目的 README 中的一段话。
“
Fast, simple, reliable. HikariCP is a "zero-overhead" production ready JDBC connection pool. At roughly 130Kb, the library is very light.
快速、简单、可靠。HikariCP是一个“零开销”的生产就绪JDBC连接池。这个库大约有130Kb,非常轻。
这个介绍真是简洁但“全面”。搭配上下边这张图。
看到这些数据,再加上Springboot 2.0 将 HikariCP 作为默认数据库连接池这件事,我已经十分好奇 HikariCP 的实现原理了。
HikariCP为什么这么快?
-
两个HikariPool
:定义了两个HikariPool对象,一个采用final类型定义,避免在获取连接时才初始化,提高性能,也避免
volatile
的额外开销。
-
FastList替代ArrayList
:采用自定义的FastList替代了ArrayList,FastList的get方法去除了范围检查逻辑,并且remove方法是从尾部开始扫描的,而并不是从头部开始扫描的。因为Connection的打开和关闭顺序通常是相反的。
-
更快的并发集合实现
:使用自定义ConcurrentBag,性能更优。
-
更快的获取连接
:同一个线程获取数据库连接时从ThreadLocal中获取,没有并发操作。
-
精简字节码
:HikariCP利用了一个第三方的Java字节码修改类库Javassist来生成委托实现动态代理,速度更快,相比于JDK 代理生成的字节码更少。
HikariCP原理
我们通过分析源码来看 HikariCP 是如何这么快的。先来看一下 HikariCP 的简单使用。
maven依赖:
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>4.0.3version>
dependency>
@Test
public void testHikariCP() throws SQLException {
// 1、创建Hikari配置
HikariConfig hikariConfig = new HikariConfig();
// JDBC连接串
hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/iam?characterEncoding=utf8");
// 数据库用户名
hikariConfig.setUsername("root");
// 数据库用户密码
hikariConfig.setPassword("123456");
// 连接池名称
hikariConfig.setPoolName("testHikari");
// 连接池中最小空闲连接数量
hikariConfig.setMinimumIdle(4);
// 连接池中最大空闲连接数量
hikariConfig.setMaximumPoolSize(8);
// 连接在池中的最大空闲时间
hikariConfig.setIdleTimeout(600000L);
// 数据库连接超时时间
hikariConfig.setConnectionTimeout(10000L);
// 2、创建数据源
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
// 3、获取连接
Connection connection = dataSource.getConnection();
// 4、获取Statement
Statement statement = connection.createStatement();
// 5、执行Sql
ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS countNum tt_user");
// 6、输出执行结果
if (resultSet.next()) {
System.out.println("countNum结果为:" + resultSet.getInt("countNum"));
}
// 7、释放链接
resultSet.close();
statement.close();
connection.close();
dataSource.close();
}
HikariConfig
:可以设置一些数据库基本配置信息和一些连接池的配置信息。
HikariDataSource
:实现了
DataSource
,DataSource是一个数据源标准或者说规范,Java所有连接池需要基于这个规范进行实现。
我们就从 HikariDataSource 开始说起。HikariDataSource有两个构造方法
HikariDataSource()
和
HikariDataSource(HikariConfig configuration)
。
private final HikariPool fastPathPool;
private volatile HikariPool pool;
public HikariDataSource()
{
super();
fastPathPool = null;
}
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();
}
HikariPool为什么要有两个(fastPathPool和pool)呢?
可以看到无参构造方法fastPathPool是null,有参构造pool = fastPathPool,采用无参构造在
getConnection()
时候才会初始化(下边会详细讲解),性能略低,并且pool是
volatile
关键字修饰,会有一些额外开销。所以建议使用有参构造。这也是HikariPool快的原因之一。
有参构造里有一行
new HikariPool(this)
,我们来看一下怎么个事。
代码太多了,往后只贴关键代码了。。。
public HikariPool(final HikariConfig config)
{
super(config);
// 初始化ConcurrentBag对象
this.connectionBag = new ConcurrentBag<>(this);
// 创建SuspendResumeLock对象
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
// 根据配置的最大连接数,创建链表类型阻塞队列
LinkedBlockingQueue 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.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
...
}
HikariPool
是为HikariCP提供基本池行为的主要连接池类。
houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS)
这行代码是 创建保持连接池连接数量的任务。该任务会关闭需要被丢弃的连接,保证最小连接数,HouseKeeper类的
run()
方法中有一行代码
fillPool()
会创建连接,我们来看一下。
创建连接
private synchronized void fillPool()
{
// 计算需要添加的连接数量
final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections()) - addConnectionQueue.size();
for (int i = 0; i // 向创建连接线程池中提交创建连接的任务
addConnectionExecutor.submit((i 1) ? poolEntryCreator : postFillPoolEntryCreator);
}
...
}
来看一下
PoolEntryCreator
是如何创建连接的。
@Override
public Boolean call()
{
// 连接池状态正常并且需求创建连接时
while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
// 创建PoolEntry对象
final PoolEntry poolEntry = createPoolEntry();
if (poolEntry != null) {
// 将PoolEntry对象添加到ConcurrentBag对象中的sharedList中
connectionBag.add(poolEntry);
return Boolean.TRUE;
}
}
...
return Boolean.FALSE;
}
PoolEntryCreator
实现了Callable接口,在
call()
方法里可以看到创建连接的过程。来继续看一下
createPoolEntry()
方法。
private PoolEntry createPoolEntry()
{
// 初始化PoolEntry对象
final PoolEntry poolEntry = newPoolEntry();
...
}
继续进入
newPoolEntry()
方法。
PoolEntry newPoolEntry() throws Exception
{
return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
}
PoolEntry构造时会先创建Connection对象传入构造函数中。PoolEntry是
ConcurrentBag
实例中用来跟踪
Connection
的。
获取链接
获取链接是通过
getConnection()
方法获取的,源码如下。
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());
}
}
}
会先去
fastPathPool
获取连接,如果
fastPathPool
为null,就会通过pool获取,如果pool也为null,会通过
双检
代码来初始化线程池。
这个上文提到过为什么两个HikariPool,fastPathPool是
final
修饰的,而pool是
volatile
修饰的,这就说明
fastPathPool
比pool性能更高,所以建议要用有参构造来创建
HikariDataSource
,才能享受到这点小细节的优化。
继续进入
HikariPool#getConnection(final long hardTimeout)
,方法中有一行关键的代码
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS)
,这行代码的作用是从
ConcurrentBag
中借出一个
PoolEntry
对象。
PoolEntry
可以看作是对
Connection
对象的封装,连接池中存储的连接其实就是一个个的
PoolEntry
。
这个
connectionBag
是用来做什么的呢?
ConcurrentBag
ConcurrentBag
是HikariCP自定义的一个无锁并发集合类。我们接着来看一下
ConcurrentBag
的成员变量。
private final CopyOnWriteArrayList sharedList;
private final boolean weakThreadLocals;
private final ThreadLocal> threadList;
private final IBagStateListener listener;
private final AtomicInteger waiters;
private volatile boolean closed;
private final SynchronousQueue handoffQueue;
回到
borrow()
方法,看一下borrow的实现逻辑。
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// 从ThreadLocal中获取当前线程绑定的对象集合,存在则获取
final List
-
先从ThreadLocal中获取以前用过的连接。ThreadLocal是当前线程的缓存,加快本地连接获取速度。