* 戳上方蓝字“后端技术互联”关注我
大家好,我是技术Z先生,一名从事后端技术十余年且热爱分享的程序员!
今天要分享的是mysql数据库连接HikariCP为何比Druid快!
引言
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.
这段简介真是简洁而全面。再配上下面这张图。

看到这些数据,再加上 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>
@Testpublic 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、获取StatementStatement statement = connection.createStatement();// 5、执行SqlResultSet 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);}...}
@Overridepublic 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;}
private PoolEntry createPoolEntry(){// 初始化PoolEntry对象final PoolEntry poolEntry = 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 的性能更高。因此,建议使用有参构造方法创建 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();}}
首先,从 ThreadLocal 中获取先前使用过的连接。ThreadLocal 是当前线程的缓存,加快了本地连接获取的速度。
如果 ThreadLocal 中没有获取到连接,会尝试从 sharedList 中获取。sharedList 是 CopyOnWriteArrayList 类型的,支持写时复制,非常适合读多写少的场景。
如果在 sharedList 中也没有获取到连接,就会进入阻塞队列等待,看是否有连接被归还可供使用。
释放连接
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();}}
为什么使用 FastList 而不是 ArrayList 呢?
FastList 去掉了范围检查:查看源码会发现,FastList 的 get() 方法比 ArrayList 少了一行代码 rangeCheck(index),这行代码用于范围检查,去掉这行代码必然会提高性能。这个细节真是令人惊叹,无处不在。
FastList 的删除操作从尾部开始:FastList 的 remove() 方法是从尾部开始扫描的,而不是从头部开始扫描的。这是因为 Connection 的打开和关闭顺序通常是相反的。FastList 的根据下标删除方法也去掉了索引范围检查。
关闭了 Statement 之后,我们回头继续往下看。
poolEntry.recycle(lastAccess);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 中获取。
总结
——The End——
今天的分享就到这里了,喜欢的小伙伴请在下方免费点赞+在看
关注我,每天学点后端技术,带你技术充点电!
对AI和后端技术感兴趣的同学可以关注以下几个公众号
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。