⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</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<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.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 < connectionsToAdd; i++) {
// 向创建连接线程池中提交创建连接的任务
addConnectionExecutor.submit((i < connectionsToAdd - 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<T> sharedList;
private final boolean weakThreadLocals;

private final ThreadLocal<List<Object>> threadList;
private final IBagStateListener listener;
private final AtomicInteger waiters;
private volatile boolean closed;

private final SynchronousQueue<T> handoffQueue;

回到borrow()方法,看一下borrow的实现逻辑。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// 从ThreadLocal中获取当前线程绑定的对象集合,存在则获取
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;
}
}

// 等待对象加一
final int waiting = waiters.incrementAndGet();
try {
// sharedList有未使用的则返回一个
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;
}
}
// sharedList没有,添加一个监听任务
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();
}
}
  1. 先从ThreadLocal中获取以前用过的连接。ThreadLocal是当前线程的缓存,加快本地连接获取速度。
  2. ThreadLocal中未获取到,会尝试从sharedList中获取,sharedList集合存在初始化的PoolEntry。sharedList是CopyOnWriteArrayList类型的,写时复制,特别适合这种读多写少的场景。
  3. sharedList中未获取到那就到阻塞队列中等着,看有没有归还的连接可以使用。

释放连接

用完连接后我们要释放,通过connection.close()释放连接,释放连接时HakariCP也做到了一些巧妙的细节。ProxyConnection的close()方法是HakariCP释放连接的实现逻辑。我们知道连接关闭前必须要关闭Statement,HakariCP对这里做了优化,来看一下代码实现。

private final FastList<Statement> openStatements;

private synchronized void closeStatements()
{
final int size = openStatements.size();
if (size > 0) {
for (int i = 0; i < size && delegate != ClosedConnection.CLOSED_CONNECTION; i++) {
try (Statement ignored = openStatements.get(i)) {
}
catch (SQLException e) {
LOGGER.warn("{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",
poolEntry.getPoolName(), delegate);
leakTask.cancel();
poolEntry.evict("(exception closing Statements during Connection.close())");
delegate = ClosedConnection.CLOSED_CONNECTION;
}
}

openStatements.clear();
}
}

存储Statement对象用的是 FastList,这也是 HakariCP 之所以快的原因之一。

为什么用FastList而不用ArrayList呢?

  • 去掉索引范围检查:查看源码会发现,FastList的get()方法比ArrayList少了一行代码rangeCheck(index),这行代码的作用是范围检查,少了这行代码必然会性能更优。不禁感叹真的是太细了啊,到处都是细节。
  • 尾部删除:FastList#remove()方法是从尾部开始扫描的,而并不是从头部开始扫描的。因为Connection的打开和关闭顺序通常是相反的。FastList的根据下标删除方法也去掉索引范围检查。

关闭掉Statement之后,我们再回过头来继续往下看。

poolEntry.recycle(lastAccess);

recycle方法会将该连接归还给线程池,recycle方法套了好几层,最终执行的是ConnectionBag的recycle方法,我们直接进入看一下。

public void requite(final T bagEntry)
{
bagEntry.setState(STATE_NOT_IN_USE);

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();
}
}

final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
}

首先将状态设置为未使用,然后判断当前是否存在等待连接的线程,如果存在则将连接加入到公平队列中,由队列中有等待连接的线程则会从阻塞队列中去获取使用;如果当前没有等待连接的线程,并且ThreadLocal中的连接小于50,则将连接添加到本地线程变量ThreadLocal缓存中,当前线程下次获取连接时直接可从ThreadLocal中获取到。

总结

这次源码探究,真的感觉看到了无数个小细节,无数个小优化,积少成多。平时开发过程中,一些小的细节也一定要“扣”。

文章目录
  1. 1. 连接池技术
  2. 2. HakariCP
  3. 3. HikariCP为什么这么快?
  4. 4. HikariCP原理
    1. 4.1. HikariPool为什么要有两个(fastPathPool和pool)呢?
    2. 4.2. 创建连接
    3. 4.3. 获取链接
    4. 4.4. ConcurrentBag
    5. 4.5. 释放连接
    6. 4.6. 为什么用FastList而不用ArrayList呢?
  5. 5. 总结