learn-tech/专栏/Java并发编程实战/41案例分析(四):高性能数据库连接池HiKariCP.md
2024-10-16 06:37:41 +08:00

12 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        41 案例分析高性能数据库连接池HiKariCP
                        实际工作中我们总会难免和数据库打交道只要和数据库打交道就免不了使用数据库连接池。业界知名的数据库连接池有不少例如c3p0、DBCP、Tomcat JDBC Connection Pool、Druid等不过最近最火的是HiKariCP。

HiKariCP号称是业界跑得最快的数据库连接池这两年发展得顺风顺水尤其是Springboot 2.0将其作为默认数据库连接池后,江湖一哥的地位已是毋庸置疑了。那它为什么那么快呢?今天咱们就重点聊聊这个话题。

什么是数据库连接池

在详细分析HiKariCP高性能之前我们有必要先简单介绍一下什么是数据库连接池。本质上数据库连接池和线程池一样都属于池化资源作用都是避免重量级资源的频繁创建和销毁对于数据库连接池来说也就是避免数据库连接频繁创建和销毁。如下图所示服务端会在运行期持有一定数量的数据库连接当需要执行SQL时并不是直接创建一个数据库连接而是从连接池中获取一个当SQL执行完也并不是将数据库连接真的关掉而是将其归还到连接池中。

数据库连接池示意图

在实际工作中我们都是使用各种持久化框架来完成数据库的增删改查基本上不会直接和数据库连接池打交道为了能让你更好地理解数据库连接池的工作原理下面的示例代码并没有使用任何框架而是原生地使用HiKariCP。执行数据库操作基本上是一系列规范化的步骤

通过数据源获取一个数据库连接; 创建Statement 执行SQL 通过ResultSet获取SQL执行结果 释放ResultSet 释放Statement 释放数据库连接。

下面的示例代码,通过 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代码HiKariCP的作者对性能的执着可见一斑不过遗憾的是他并没有详细解释都做了哪些优化。而宏观上主要是和两个数据结构有关一个是FastList另一个是ConcurrentBag。下面我们来看看它们是如何提升HiKariCP的性能的。

FastList解决了哪些性能问题

按照规范步骤执行完数据库操作之后需要依次关闭ResultSet、Statement、Connection但是总有粗心的同学只是关闭了Connection而忘了关闭ResultSet和Statement。为了解决这种问题最好的办法是当关闭Connection时能够自动关闭Statement。为了达到这个目标Connection就需要跟踪创建的Statement最简单的办法就是将创建的Statement保存在数组ArrayList里这样当关闭Connection的时候就可以依次将数组中的所有Statement关闭。

HiKariCP觉得用ArrayList还是太慢当通过 conn.createStatement() 创建一个Statement时需要调用ArrayList的add()方法加入到ArrayList中这个是没有问题的但是当通过 stmt.close() 关闭Statement的时候需要调用 ArrayList的remove()方法来将其从ArrayList中删除这里是有优化余地的。

假设一个Connection依次创建6个Statement分别是S1、S2、S3、S4、S5、S6按照正常的编码习惯关闭Statement的顺序一般是逆序的关闭的顺序是S6、S5、S4、S3、S2、S1而ArrayList的remove(Object o)方法是顺序遍历查找,逆序删除而顺序查找,这样的查找效率就太慢了。如何优化呢?很简单,优化成逆序查找就可以了。

逆序删除示意图

HiKariCP中的FastList相对于ArrayList的一个优化点就是将 remove(Object element) 方法的查找顺序变成了逆序查找。除此之外FastList还有另一个优化点是 get(int index) 方法没有对index参数进行越界检查HiKariCP能保证不会越界所以不用每次都进行越界检查。

整体来看FastList的优化点还是很简单的。下面我们再来聊聊HiKariCP中的另外一个数据结构ConcurrentBag看看它又是如何提升性能的。

ConcurrentBag解决了哪些性能问题

如果让我们自己来实现一个数据库连接池最简单的办法就是用两个阻塞队列来实现一个用于保存空闲数据库连接的队列idle另一个用于保存忙碌数据库连接的队列busy获取连接时将空闲的数据库连接从idle队列移动到busy队列而关闭连接时将数据库连接从busy移动到idle。这种方案将并发问题委托给了阻塞队列实现简单但是性能并不是很理想。因为Java SDK中的阻塞队列是用锁实现的而高并发场景下锁的争用对性能影响很大。

//忙碌队列 BlockingQueue busy; //空闲队列 BlockingQueue idle;

HiKariCP并没有使用Java SDK中的阻塞队列而是自己实现了一个叫做ConcurrentBag的并发容器。ConcurrentBag的设计最初源自C#它的一个核心设计是使用ThreadLocal避免部分并发问题不过HiKariCP中的ConcurrentBag并没有完全参考C#的实现,下面我们来看看它是如何实现的。

ConcurrentBag中最关键的属性有4个分别是用于存储所有的数据库连接的共享队列sharedList、线程本地存储threadList、等待数据库连接的线程数waiters以及分配数据库连接的工具handoffQueue。其中handoffQueue用的是Java SDK提供的SynchronousQueueSynchronousQueue主要用于线程之间传递数据。

//用于存储所有的数据库连接 CopyOnWriteArrayList sharedList; //线程本地存储中的数据库连接 ThreadLocal<List