HikariCP:一个叫光的JDBC连接池

简介

天不生我李淳罡,剑道万古如长夜。

Hikari [hi·ka·'lē] 是日语“光”的意思。HikariCP的卖点是快、简洁、可靠,整体非常轻量,只有130Kb左右。

HikariCP的出现可以说是颠覆了连接池领域,直接将性能做到极致,从此之后再无人可做出大的突破。就连曾经风靡一时的BoneCP也主动停止维护,让贤于他。而且,在Spring Boot 2.0中,HikariCP凭借其优越的性能取代Tomcat成为默认的数据库连接池。
BoneCP
HikariCP的性能如下图所示,每毫秒可执行5万次左右的连接操作(DataSource.getConnection()/Connection.close()),是c3p0的200倍。每毫秒可执行14.6万+的Statement操作(Connection.prepareStatement(), Statement.execute(), Statement.close())。dbcp2和Tomcat在Statement基准测试中由于超过了GC次数上限导致OutOfMemoryError而未能完成测试。
HikariCP性能对比
HikariCP用户反馈的部署前后连接的稳定性对比图。
HikariVsBone

连接池

以史为鉴,可以知兴替。

连接池(Connection Pool)技术的核心思想就是:连接复用,通过建立一个数据库连接池以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。

连接池

在不使用连接池的情况下,每次请求都需要经过如下过程:

  1. 与MySQL服务器三次握手建立TCP连接;
  2. 登录认证建立MySQL连接;
  3. 执行SQL语句获取结果;
  4. 断开与MySQL的连接;
  5. 关闭TCP连接。

使用连接池后只需要在初始化时执行一次建立TCP连接和登录认证流程,之后连接池会维护指定数量的连接资源,当有请求时直接从连接池中获取连接执行SQL即可,SQL执行完毕后归还连接给连接池,而无需关闭连接,待应用关闭时由连接池负责将申请的连接资源释放即可。

数据库连接池相关技术发展至今已非常成熟了,使用比较广泛的有BoneCP、DBCP、C3P0、Druid等。

C3P0

C3P0(Why C3P0)在很长一段时间内一直是Java领域内数据库连接池的代名词,当年盛极一时的Hibernate都将其作为内置的数据库连接池,可见业内对它还是认可的。C3P0的功能简单易用、稳定性好,但是性能上的缺点却让它最终被打入冷宫。C3P0的性能差到即便是和同时代的产品相比它也是垫底的,更不用和Druid、HikariCP相比了。正常来讲,有问题很正常,改就是了,但C3P0最致命的问题就是架构设计过于复杂,让重构变成了一项不可能完成的任务。最终,性能有硬伤的C3P0也彻底的退出了历史舞台。

DBCP

DBCP(DataBase Connection Pool)属于Apache顶级项目Commons中的核心子项目,在Apache的生态圈中的影响十分广泛,Tomcat就在其内部集成了DBCP,实现JPA规范的OpenJPA,也默认集成了DBCP。但DBCP并不是独立实现连接池功能的,它内部依赖于Commons中的另一个子项目pool。连接池最核心的“池”,就是由pool组件提供的,因此,DBCP的性能实际上就是pool的性能。但有很长一段时间pool都停留在1.x版本,导致DBCP也更新乏力,许多依赖DBCP的应用在遇到性能瓶颈后别无选择,只能将其替换掉。Tomcat就在其7.0版本中重新设计开发了一套连接池–Tomcat JDBC Pool

2013年9月Commons-Pool 2.0 版本发布,事情迎来转机,DBCP 2.0版本在2014年2月发布,基于新的线程模型全新设计的“池”让DBCP重焕青春,虽然和新一代的连接池相比仍有一定差距,但DBCP 2.x版本已经稳稳达到了和新一代产品同级别的性能指标。

然而长时间的等待已经消磨了用户的耐心,DBCP2与其他产品相比没有任何突出优势。试问,谁会在有选择的前提下,去选择那个并不优秀的呢?如果有,那也只是情怀吧。

BoneCP

在本文开头已提到过BoneCP,它是一个以高性能著称的JDBC连接池。BoneCP的高性能一是源自其极简的设计,整个产品只有几百k大小,二是其重构了内部pool的设计,减少了锁的使用。这两点优化原则,几乎适用于所有的连接池产品。

值得一提的是,BoneCP本身并不“健全”,它的很多特征都依赖于Guava,因此也和DBCP一样面临更新乏力的问题。但现在这些都不重要了,BoneCP引以为傲的性能已被HikariCP全面超越,已主动让贤于与自己设计思路类似的HikariCP。

精简的设计

有道无术,术尚可求,有术无道,止于术。

HikariCP崇尚的设计美学是极简主义,遵循KISS (Keep It Simple Stupid) 的设计哲学。

对连接池来说,要想在性能上做出改进,最直接的做法可能是优化获取连接的逻辑。然而HikariCP获取连接的逻辑和其他连接池的差别并不大,其大部分性能提升来自于对Connection,Statement 等的委托(delegates)的优化。具体包括以下几个方面。
HikariCP

字节码优化

Java的编译过程分为两部分,首先由javac将代码编译成字节码,然后再由解释器将字节码解释为机器码来执行。所以在性能上,Java通常不如C++这类将代码直接编译成CPU所能理解的机器码的编译型语言。为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。

JIT

HikariCP的作者研究了编译器的字节码输出,JIT的汇编输出,将关键链路限制在JIT内联阈值以下。并且扁平化了继承层次结构,隐藏了成员变量,消除了类型转换。

ArrayList–>FastList

减少ConnectionProxy中用来追踪Statement实例的ArrayList实例,当一个Statement被关闭的时候,必须将它从集合中移出,当集合被关闭的时候,必须迭代集合将其中打开的所有statement实例,然后才能清空集合。Java中的ArrayList在get(int index)调用时会进行边界检测,HikariCP中能保证边界,因此这个检查就是无意义的。

另外,在remove(Object)的实现中会从头到尾进行扫描,然而,在JDBC中通常会在使用完后立即关闭Statements或按打开的相反顺序进行关闭,因此从尾部开始扫描性能会更好。因此,HikariCP中用自定义的FastList来替换ArrayList,FastList不进行边界检测且删除元素时从尾部开始扫描。

ConcurrentBag

HikariCP中包含了一个自定义的无锁集合ConcurrentBag。无锁设计、ThreadLocal缓存、

Queue-stealing、hand-off优化使得ConcurrentBag具有高并发、低延迟、最小化伪共享(false-sharing)的特性。

ConcurrentBag是HikariCP连接池中存放连接的容器,属于核心内容,其具体实现我们留到后面代码分析时再进行详细说明。

代理实现

为了为 Connection、Statement 和 ResultSet 实例生成代理,HikariCP 最初使用了一个以ConnectionProxy形式维护在静态字段 (PROXY_FACTORY) 中的单例工厂。有十几个类似的方法:

public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
   
    return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}

用单例工厂生成的字节码如下:

    public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
    flags: ACC_PRIVATE, ACC_FINAL
    Code:
      stack=5, locals=3, args_size=3
         0: getstatic     #59                 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
         3: aload_0
         4: aload_0
         5: getfield      #3                  // Field delegate:Ljava/sql/Connection;
         8: aload_1
         9: aload_2
        10: invokeinterface #74,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
        15: invokevirtual #69                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
        18: return

可以看到先是getstatic调用拿到静态字段PROXY_FACTORY的值,最后invokevirtual调用ProxyFactory实例中的getProxyPreparedStatement()方法。

去掉单例工厂将其替换为有着静态方法(由Javassist生成具体实现)的final类后,Java代码变为:

    public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
    {
   
        return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
    }

生成的字节码变为:

    private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
    flags: ACC_PRIVATE, ACC_FINAL
    Code:
      stack=4, locals=3, args_size=3
         0: aload_0
         1: aload_0
         2: getfield      #3                  // Field delegate:Ljava/sql/Connection;
         5: aload_1
         6: aload_2
         7: invokeinterface #72,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
        12: invokestatic  #67                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
        15: areturn

两相对比可看出:

  1. getstatic调用没有了;

  2. invokevirtual调用替换成了更容易被JVM优化的invokestatic;

  3. 堆栈大小从5个元素减少到4个元素。这是因为invokevirtual会先将ProxyFactory实例压入堆栈,当调用getProxyPreparedStatement()时再将其弹出。

Statement Cache

在许多连接池中都会提供PreparedStatement缓存,而HikariCP中则去掉了这个缓存。在连接池层使用statement cache是一个反面模式 (Anti-pattern),与driver层提供的缓存相比,会对应用程序的性能产生负面影响。

在连接池层 PreparedStatements 只能缓存每个连接。如果有 250 个经常执行的查询和一个包含 20 个连接的池,则数据库需要保留 5000 个查询执行计划。同理,连接池必须缓存这么多 PreparedStatements 及其相关的对象图。

大多数主流数据库 JDBC 驱动程序(PostgreSQL、Oracle、MySQL等)已经有一个可以配置的statement缓存。 JDBC 驱动程序处于利用数据库特定功能的独特位置,几乎所有缓存实现都能够跨连接共享执行计划。这意味着这 250 个经常执行的查询会在数据库中产生恰好 250 个执行计划,而不是内存中的 5000 个语句和相关的执行计划。聪明的实现甚至不会在驱动程序级别的内存中保留 PreparedStatement 对象,而只是将新实例附加到现有计划 ID。

Scheduler quanta

当‘一次’运行的线程数超过CPU核数时,操作系统会给每个线程分配一个小的运行时间片,并在线程间切换调度(这样一次切换叫做一个quantum)。当一个时间片用完后,在调度器再给该线程分配下一个时间片之前可能要等待一段‘长’的时间,因此,一个线程要尽可能在他自己的时间片里完成,并避免锁强制它放弃自己的时间片就显得至关重要,否则就会有大的性能损耗。

CPU缓存行失效

CPU缓存行失效是除了锁之外,另一个会导致线程无法在一个quanta中完成的因素。

当线程被调度器再次唤起时,它确实获得了再次在其经常访问的数据上运行的机会,然而这些数据可能已经不再位于CPU的L1 或 L2 缓存中了,因为我们没法控制下次调度会被分配给哪个CPU核。

优雅的实现

这一拳二十年的功夫,你挡得住吗? --《霍元甲》

HikariCP 的源码少且精,可读性非常高。如果你想提升自己的Java多线程编程能力,可以来看看HikariCP的源码,字里行间皆透露出作者是一个具有多年多线程编程实战经验的高手。

核心类

HikariPool负责对连接的管理,其中维护了一个ConcurrentBag,里面存放着连接对象PoolEntry,getConnection逻辑就是从ConcurrentBag中borrow一个可用的连接PoolEntry,再经过ProxyFactory对连接实例进行处理后返回代理连接给HikariDataSource使用。

class
hikaricp-overview

获取连接

  • HikariDataSource#getConnection
    getConnection

说明:

  1. 该方法的主体是使用 双重检查锁定模式 获取HikariPool对象,如果对象为空则进行初始化,实际getConnection逻辑在HikariPool中;

  2. 从上述流程图可知HikariCP是在第一次getConnection时对HikariPool进行的初始化,一旦启动相应配置就固化了(sealed),不允许再次修改(除非使用HikariConfigMXBean方法);

  3. HikariPool类型的对象有两个:fastPathPool和pool,如果使用的是HikariDataSource的默认构造函数则fastPathPool为null,如果使用指定配置的构造函数则pool和fastPathPool等价。二者的差别在于getConnection时pool会因为延迟初始化检查导致性能有轻微下降。

  • HikariPool#getConnection

getConn

说明:

  1. suspendResumeLock:通过对信号量进行简单封装实现了一个锁,用于控制获取连接的频率。如果isAllowPoolSuspension配置为true则创建一个最大并发量10000的Semaphore,否则给一个假的锁实现。

  2. 获取poolEntry后判断其是否可用时,如果连接没有被标记为evicted,则会进行连接判活,判断连接距上次访问的时间间隔是否大于500ms(默认值,可配置),如果大于则继续判断连接是否dead。从中可看出HikariCP的连接判活还是挺频繁的,这说明连接判活对其性能影响并不大,这也是得益于其无锁实现。

  3. isConnectionDead方法用于判断连接是否已失活,需要注意的是这里的连接不是PoolEntry对象,而是其持有的实际驱动的Connection对象。如果支持jdbc4协议,则执行驱动程序的isValid判断,否则执行开销较大的createStatement+execute的逻辑。比较取巧的是HikariCP是通过connectionTestQuery是否为null来判断是否支持jdbc4协议的。因此,如果是jdbc4驱动的话不要配置connectionTestQuery,这个配置只是给旧的不支持Connection.isValid的版本使用的

  4. HikariPool中维护了一个ConcurrentBag,里面存放着连接对象PoolEntry,getConnection逻辑就是从ConcurrentBag中borrow一个可用的连接PoolEntry。

初始化池对象

public HikariPool(final HikariConfig config)
   {
   
      super(config); // 继承自PoolBase,基本属性设置
      // initializeDataSource 底层DataSource设置,jdbcUrl、username、password等

      this.connectionBag = new ConcurrentBag<>(this); // 真正存放连接的对象,核心!
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
      // 初始化一个ScheduledExecutorService,用于Housekeeping:
      // 连接泄露检测、定时清理闲置连接、延迟关闭连接 

      checkFailFast(); // initializationFailTimeout>0 则进行DB连通性检测

      if (config.getMetricsTrackerFactory() != null) {
   
         setMetricsTrackerFactory(config.getMetricsTrackerFactory());
      }
      else {
   
         setMetricRegistry(config.getMetricRegistry());
      }

      setHealthCheckRegistry(config.getHealthCheckRegistry());

      handleMBeans(this, true);

      
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值