实际工做中,咱们总会不免和数据库打交道;只要和数据库打交道,就免不了使用数据库链接池。业界知名的数据库链接池有很多,例如 DBCP、Tomcat JDBC Connection Pool、Druid 等,不过最近最火的是 HiKariCP。git
HiKariCP 号称是业界跑得最快的数据库链接池,尤为是 Springboot 2.0 将其做为默认数据库链接池
后,地位已经是毋庸置疑了。那它为何那么快呢?带着问题咱们来看下。github
在详细分析 HiKariCP 高性能以前,咱们有必要先简单介绍一下什么是数据库链接池。本质上,数据库链接池和线程池同样,都属于池化资源,做用都是避免重量级资源的频繁建立和销毁,对于数据库链接池来讲,也就是避免数据库链接频繁建立和销毁。服务端会在运行期持有必定数量的数据库链接,当须要执行 SQL 时,并非直接建立一个数据库链接,而是从链接池中获取一个;当 SQL 执行完,也并非将数据库链接真的关掉,而是将其归还到链接池中。数据库
为了能让你更好地理解数据库链接池的工做原理,咱们不使用使用任何框架,而是原生地使用 HiKariCP。执行数据库操做基本上是一系列规范化的步骤:数组
下面的示例代码,经过 ds.getConnection()
获取一个数据库链接时,实际上是向数据库链接池申请一个数据库链接,而不是建立一个新的数据库链接。一样,经过 conn.close()
释放一个数据库链接时,也不是直接将链接关闭,而是将链接归还给数据库链接池。数据结构
// 数据库链接池配置 HikariConfig config = new HikariConfig(); config.setMinimumIdle(1); config.setMaximumPoolSize(2); config.setConnectionTestQuery("SELECT 1"); config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); config.addDataSourceProperty("url", "jdbc:h2:mem:test"); // 建立数据源 DataSource ds = new HikariDataSource(config); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 获取数据库链接 conn = ds.getConnection(); // 建立 Statement stmt = conn.createStatement(); // 执行 SQL rs = stmt.executeQuery("select * from abc"); // 获取结果 while (rs.next()) { int id = rs.getInt(1); ...... } } catch(Exception e) { e.printStackTrace(); } finally { // 关闭 ResultSet close(rs); // 关闭 Statement close(stmt); // 关闭 Connection close(conn); } // 关闭资源 void close(AutoCloseable rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } }
HiKariCP官文解释了其性能之因此如此之高的秘密。微观上 HiKariCP 程序编译出的字节码执行效率更高,站在字节码的角度去优化 Java 代码。而宏观上主要是和两个数据结构有关,一个是 FastList,另外一个是 ConcurrentBag。并发
按照规范步骤,执行完数据库操做以后,须要依次关闭 ResultSet、Statement、Connection,可是总有粗心的同窗只是关闭了 Connection,而忘了关闭 ResultSet 和 Statement。为了解决这种问题,最好的办法是当关闭 Connection 时,可以自动关闭 Statement。为了达到这个目标,Connection 就须要跟踪建立的 Statement,最简单的办法就是将建立的 Statement 保存在数组 ArrayList 里,这样当关闭 Connection 的时候,就能够依次将数组中的全部 Statement 关闭。框架
HiKariCP 以为用 ArrayList 仍是太慢。 由于 当经过 stmt.close()
关闭 Statement 的时候,须要调用 ArrayList 的 remove() 方法来将其从 ArrayList 中删除,这里是有优化余地的。高并发
假设一个 Connection 依次建立 6 个 Statement,分别是 S一、S二、S三、S四、S五、S6,按照正常的编码习惯,关闭 Statement 的顺序通常是逆序的,关闭的顺序是:S六、S五、S四、S三、S二、S1,而 ArrayList 的 remove(Object o) 方法是顺序遍历查找,逆序删除而顺序查找,这样的查找效率就太慢了。如何优化呢?很简单,优化成逆序查找就能够了。工具
HiKariCP 中的 FastList 相对于 ArrayList 的一个优化点就是将remove(Object element)
方法的查找顺序变成了逆序查找
。除此以外,FastList 还有另外一个优化点,是 get(int index)
方法没有对 index 参数进行越界检查,HiKariCP 能保证不会越界,因此不用每次都进行越界检查。性能
若是让咱们本身来实现一个数据库链接池,最简单的办法就是用两个阻塞队列来实现,一个用于保存空闲数据库链接的队列 idle,另外一个用于保存忙碌数据库链接的队列 busy;获取链接时将空闲的数据库链接从 idle 队列移动到 busy 队列,而关闭链接时将数据库链接从 busy 移动到 idle。这种方案将并发问题委托给了阻塞队列,实现简单,可是性能并非很理想。由于 Java SDK 中的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。
// 忙碌队列 BlockingQueue<Connection> busy; // 空闲队列 BlockingQueue<Connection> idle;
HiKariCP 并无使用 Java SDK 中的阻塞队列,而是本身实现了一个叫作 ConcurrentBag 的并发容器。它的一个核心设计是使用 ThreadLocal 避免部分并发问题, 下面咱们来看看它是如何实现的。
ConcurrentBag 中最关键的属性有 4 个,分别是:用于存储全部的数据库链接的共享队列 sharedList、线程本地存储 threadList、等待数据库链接的线程数 waiters 以及分配数据库链接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于线程之间传递数据。
// 用于存储全部的数据库链接 CopyOnWriteArrayList<T> sharedList; // 线程本地存储中的数据库链接 ThreadLocal<List<Object>> threadList; // 等待数据库链接的线程数 AtomicInteger waiters; // 分配数据库链接的工具 SynchronousQueue<T> handoffQueue;
当线程池建立了一个数据库链接时,经过调用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,下面是 add() 方法的具体实现,逻辑很简单,就是将这个链接加入到共享队列 sharedList 中,若是此时有线程在等待数据库链接,那么就经过 handoffQueue 将这个链接分配给等待的线程。
// 将空闲链接添加到队列 void add(final T bagEntry){ // 加入共享队列 sharedList.add(bagEntry); // 若是有等待链接的线程, // 则经过 handoffQueue 直接分配给等待的线程 while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) { yield(); } }
经过 ConcurrentBag 提供的 borrow() 方法,能够获取一个空闲的数据库链接,borrow() 的主要逻辑是:
线程本地存储中的链接是能够被其余线程窃取的,因此须要用 CAS 方法防止重复分配。在共享队列中获取空闲链接,也采用了 CAS 方法防止重复分配。
T borrow(long timeout, final TimeUnit timeUnit){ // 先查看线程本地存储是否有空闲链接 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; // 线程本地存储中的链接也能够被窃取, // 因此须要用 CAS 方法防止重复分配 if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } } // 线程本地存储中无空闲链接,则从共享队列中获取 final int waiting = waiters.incrementAndGet(); try { for (T bagEntry : sharedList) { // 若是共享队列中有空闲链接,则返回 if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } } // 共享队列中没有链接,则须要等待 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); // 超时没有获取到链接,返回 null return null; } finally { waiters.decrementAndGet(); } }
释放链接须要调用 ConcurrentBag 提供的 requite() 方法,该方法的逻辑很简单,首先将数据库链接状态更改成 STATE_NOT_IN_USE,以后查看是否存在等待线程,若是有,则分配给等待线程;若是没有,则将该数据库链接保存到线程本地存储里。
// 释放链接 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 { yield(); } } // 若是没有等待的线程,则进入线程本地存储 final List<Object> threadLocalList = threadList.get(); if (threadLocalList.size() < 50) { threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); } }
HiKariCP 中的 FastList 和 ConcurrentBag 这两个数据结构使用得很是巧妙,虽然实现起来并不复杂,可是对于性能的提高很是明显,根本缘由在于这两个数据结构适用于数据库链接池这个特定的场景。FastList 适用于逆序删除场景;而 ConcurrentBag 经过 ThreadLocal 作一次预分配,避免直接竞争共享资源,很是适合池化资源的分配。