作者 | 黄永灿

后端开发攻城狮,关注服务端技术与性能优化。

前言

在我们的工作中,免不了要和数据库打交道,而要想和数据库打好交道,选择一款合适的数据库连接池就至关重要,目前业界广为人知的数据库连接池有 Tomcat JDBC Connection Pool、c3p0、DBCP、BoneCP、Druid 等,而我们这次要介绍的主角是 HiKariCP,HiKariCP 号称业界跑得最快的数据库连接池,近几年发展的风生水起,更是被 Spring Boot 2.0 选中作为其默认数据库连接池,基本上是坐实了江湖一哥的地位,今天咱们就来分析一下为什么它能跑得这么快。

HiKariCP 全称 HiKari Connection Pool,HiKari 源自日语 - 光,你可以这样读 hi-ka-le

有多快

以下数据摘自 HikariCP 官方,可以看到,不管是获取-关闭数据库连接还是执行语句,其速度均远高于其他产品。

什么是数据库连接池

在揭 HiKari 老底之前,我们先简单介绍(回顾)一下什么是数据库连接池。我们都知道在 Java 里面,所有的线程创建和调度都是委托给操作系统的,也就是说 Java 里面的线程是和操作系统的线程一一对应的,这样做的好处是稳定可靠,因为操作系统在这方面非常成熟,但缺点也是显而易见的,创建成本太高了,所以我们想办法弄出了各种各样的线程池,而本质上,数据库连接池和线程池一样,都属于池化资源,作用都是避免重量级资源的频繁创建和销毁,只不过对于数据库连接池来说,这个重量级资源不是线程而是数据库连接。

当我们使用数据库连接池后,在程序运行时连接池会保有一定数量的数据库连接,当需要执行 SQL 时,并不是直接创建一个数据库连接,而是从连接池中获取一个,当 SQL 执行完,再把这个数据库连接归还给连接池。

为什么这么快

为什么 HiKariCP 能跑这么快?实际上 JDBC 连接池的实现并不复杂,主要是对 JDBC 中几个核心对象 Connection、Statement、PreparedStatement、CallableStatement 以及 ResultSet 的封装与动态代理,能够优化的空间不大,HiKariCP 究竟是有什么本领能在极少的代码量(仅两千多行)上做到比其他数据库连接池快那么多呢?

1.优化字节码

HikariCP 对 java.sql.* 提供了五个代理类

  1. ProxyConnection (proxy class for java.sql.Connection)

  2. ProxyStatement (proxy class for java.sql.Statement)

  3. ProxyPreparedStatement (proxy class for java.sql.PreparedStatement)

  4. ProxyCallableStatement (proxy class for java.sql.CallableStatement)

  5. ProxyResultSet (proxy class for java.sql.ResultSet)

然后再提供了一个 ProxyFactory 来获得这几个代理类,但当我们查看 ProxyFactory 源码时会发现,其方法体里面都是直接抛异常,而没有具体实现,以 getProxyStatement 举例。

static Statement getProxyStatement(final ProxyConnection connection, final Statement statement) {// Body is replaced (injected) by JavassistProxyFactory// 方法 body 中的代码在编译时调用 JavassistProxyFactory 生成throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}

HiKariCP 觉得用 JDK 代理和 CGLIB 代理还是太慢了,所以利用了一个第三方的 Java 字节码修改类库 Javassist 来生成委托实现动态代理,其生成出来的字节码更少更精简,动态代理性能大概是 CGLIB 代理的五倍,JDK 代理(jdk1.8之前)的十倍。具体性能对比可以参考这篇博客动态代理方案性能对比。

2.自定义并发容器 ConcurrentBag

在介绍 ConcurrentBag 容器前,我们先来想一下,如果让来实现一个数据库连接池,我们会采用何种数据结构?比较简单的办法就是使用两个阻塞队列 a 和 b 分别存储空闲的数据库连接和使用中的数据库连接,getConnection 时将数据库连接从 a 队列移至 b 队列,connection.close 时再将数据库连接从 b 移至 a。这种方案实现起来简单,但是性能并不是很理想,因为 Java 里面的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。

ConcurrentBag 是 HiKariCP 专门为连接池设计的一个 lock-less 集合,实现了比阻塞队列更好并发性能,它的核心思想是使用 ThreadLocal 来避免一定的并发问题,其主要结构如下。

public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {// 用于存储所有的数据库连接private final CopyOnWriteArrayList<T> sharedList;// 线程本地存储中的数据库连接private final ThreadLocal<List<Object>> threadList;// 用于存在资源等待线程时的第一手资源交接private final SynchronousQueue<T> handoffQueue;
}

CopyOnWriteArrayList 是 juc 包里面的一个线程安全的集合,基于 Copy-On-Write 思想,读操作完全无锁,写操作时加锁复制一份数据出来修改,再替换原先的数据,在写加锁期间,并不会影响读操作,只不过读操作读的依旧是老数据。

SynchronousQueue 是一个是一个无存储空间的阻塞队列,非常适合做交换工作,经常用于生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。因为是无存储空间的,所以与其他阻塞队列实现不同的是,这个阻塞 peek 方法直接返回 null,无任何其他操作,其他的方法与阻塞队列一致。这个队列的特点是,必须先调用 take 或者 poll 方法,才能使用 offer 和 put 方法,感兴趣可以去看下源码。

当数据库连接池初始化或扩容的时候,会创建一个新的数据库连接(即 T bagEntry)并调用 ConcurrentBag 的 add 方法将其添加到 sharedList 中,如果这时有线程正在等待获取数据库连接,则通过 handoffQueue 将这个连接分配给等待的线程。

public void add(final T bagEntry) {if (closed) {LOGGER.info("ConcurrentBag has been closed, ignoring add()");throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");}// 加入共享队列sharedList.add(bagEntry);// spin until a thread takes it or none are waiting// 如果有等待连接的线程,则通过 handoffQueue 直接分配给等待的线程while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {yield();}
}

当我们想从连接池获取一个数据库连接时,HiKari 会调用 ConcurrentBag 的 borrow 方法,borrow 方法的主要逻辑是:

  1. 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接;

  2. 如果线程本地存储中无空闲连接,则从共享队列中获取。

  3. 如果共享队列中也没有空闲的连接,则请求线程需要等待。

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {// Try the thread-local list first// 先查看线程本地存储是否有空闲连接final List<Object> list = threadList.get();for (int i = list.size() - 1; i >= 0; i--) {final Object entry = list.remove(i);// weakThreadLocals 是用于判断 ThreadLocal 里面存的是连接的弱引用还是强引用// 可以通过配置项 com.zaxxer.hikari.useWeakReferences 设置,但官方不推荐覆盖// 当 ConcurrentBag 的类加载器和系统的类加载器不一样时是 true,默认是 falsefinal T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;// 线程本地存储中的连接也可以被窃取(下文会解释到),所以需要用 CAS 防止重复分配if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {return bagEntry;}}// Otherwise, scan the shared list ... then poll the handoff queue// 线程本地存储中无空闲连接,则从共享队列中获取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);// 超时没有获取到连接,返回 nullreturn null;} finally {waiters.decrementAndGet();}
}

当我们执行完 SQL 释放数据库连接时,会调用 ConcurrentBag 的 requite 方法,该方法的逻辑很简单,首先将数据库连接状态更改为 STATENOTIN_USE,之后查看是否存在等待线程,如果有,则分配给等待线程;如果没有,则将该数据库连接保存到线程本地存储里。

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 {yield();}}// 如果没有等待的线程,则进入线程本地存储final List<Object> threadLocalList = threadList.get();if (threadLocalList.size() < 50) {threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);}
}

这里解释一下为什么说 ThreadLocal 存储的数据库连接是可以被其他线程窃取的,因为在调用 borrow 方法时,如果当前线程自己的 ThreadLocal 没有空闲的连接,则会去 sharedList 里面去找空闲的连接,这个连接有可能已经在其他线程的 ThreadLocal 里面,然后在调用 requite 方法时,如果没有等待的线程,当前线程会把这个连接加入到自己的 ThreadLocal 里面,也就是说一个数据库连接可能被多个线程的 ThreadLocal 引用。

3.自定义数组 FastList

FastList 是 HikariCP 自己实现的用来替代 ArrayList 的一个集合,主要用在 ConcurrentBag 的 threadList 上和 Connection 存储 Statement 上,因为他们觉得 ArrayList 性能不够好,没错,就是性能不满意。FastList 主要做了两个地方的优化:

  1. get(int index) 方法去掉对 index 参数进行越界检查,因为 HikariCP 能保证不越界,只会在 for 循环里面用到

  2. remove(Object element) 方法由顺序遍历查找改为逆序遍历查找

第一个优化大家应该很容易理解,能少执行一个判断,当调用很频繁时,其效果就很明显了。第二个优化跟数据库连接池的业务有很大关系,当我们执行完 SQL 后,按照规范,需要关闭 Connection 和 Statement,而关闭 Statement 时需要将 Statement 从 Connection 以逆序的方式移除,如果按照 ArrayList 的 remove 方式,将 n 个 Statement 移除总共要 n + n-1 + ... + 1 次,而如果改为逆序,则只需要 n 次。

总结

除了以上说的几个主要优化之外,HikariCP 还做了若干细节上的优化,包括优化拦截器、对耗时超过一个 CPU 时间片的方法优化等,当然,写本文的目的也不是为了推广 HikariCP 数据库连接池,而是希望学习其中的思想,毕竟 Druid 虽然性能不如 HikariCP,但自带各种监控功能不香嘛,选择一个适合业务的才是最重要的。

参考

  1. HikariCP 官网

  2. Java并发编程实战

全文完


以下文章您可能也会感兴趣:

  • 聊聊Hystrix 命令执行流程

  • Mysql redo log 漫游

  • RabbitMQ 如何保证消息可靠性

  • 复杂业务状态的处理:从状态模式到 FSM

  • 简单聊聊 TCP 的可靠性

  • 延时队列:基于 Redis 的实现

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

  • Actor 模型及 Akka 简介

  • 从零搭建一个基于 lstio 的服务网格

  • 容器管理利器:Web Terminal 简介

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

HikariCP 了解一下相关推荐

  1. 干掉ArrayList:HikariCP为什么自己造了一个FastList?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! HikiriCP作为当今世界上最快的数据库连接池中间件,其 ...

  2. 跟我学Springboot开发后端管理系统4:数据库连接池Druid和HikariCP

    上一篇文章主要讲解了如何再Matrix-Web中使用Mybatis-Plus,Mybatis-Plus作为Orm框架,连接数据库需要连接数据库的依赖.WEB 系统高并发环境下,频繁的进行数据库连接操作 ...

  3. Spring Boot 青睐的数据库连接池HikariCP为什么是史上最快的?

    前言 现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及. 下面陈某带大家从 ...

  4. HikariCP为什么自己造了一个FastList?

    HikiriCP作为当今世界上最快的数据库连接池中间件,其对代码追求的极致一直被开源爱好者津津乐道.HikariCP之所以这么快的其中一个原因就是:开发FastList取代ArrayList.那么Fa ...

  5. Spring Boot 2.0选择HikariCP作为默认数据库连接池的五大理由

    转载自公众号:工匠小猪猪的技术世界 摘要: 本文非原创,是「工匠小猪猪的技术世界」搜集了一些HikariCP相关的资料整理给大家的介绍,主要讲解了为什么sb2选择了HikariCP以及HikariCP ...

  6. SpringBoot中使用hikariCP

    本篇文章主要实现SpringBoot中使用hikariCP:  一 .使用工具  1. JDK1.8  2. springToolSuit(STS)  3. maven  二.创建项目  1.首先创建 ...

  7. java 革命_JAVA数据库连接池的革命 -- 从BoneCP到HikariCP(转)

    从BoneCP到HikariCP 今天笔者本想更新一下项目中使用到的BoneCP版本的.却无意发现jolbox网站打不开了.起初以为是被墙掉了,经过一番查找,居然在BoneCP的Github站看到了如 ...

  8. hikaricp 连接池分析_数据库连接池终于搞对了,这次直接从100ms优化到3ms!

    我在研究HikariCP(一个数据库连接池)时无意间在HikariCP的Github wiki上看到了一篇文章(即前面给出的链接),这篇文章有力地消除了我一直以来的疑虑,看完之后感觉神清气爽.故在此做 ...

  9. HikariCP 高性能的 JDBC 连接池

    HikariCP 是一个高性能的 JDBC 连接池组件.下图是性能的比较测试结果: 使用方法: HikariConfig config = new HikariConfig(); config.set ...

  10. HikariCP连接池配置

    2019独角兽企业重金招聘Python工程师标准>>> HikariCP号称性能最好的Java数据库连接池.虽没做过亲测但是公司项目一直在用,大概经历过2万左右用户同时在线,链接池性 ...

最新文章

  1. laravel oauth2.0 文件上传报错
  2. Java虚拟机 —— 垃圾回收机制
  3. SAP Spartacus 代码提交的git message规范
  4. c语言中if和goto的用法,C语言中if和goto的用法.doc
  5. 宛如造句,小学生怎么用宛如造句?
  6. 德国政府证实警方使用了 Pegasus 间谍软件
  7. php使用RabbitMQ
  8. Atitit  godaddy 文件权限 root权限设置
  9. 毕设题目:Matlab优化覆盖
  10. ubuntu常用命令(未整理)
  11. 端口打流互通功能测试
  12. 怎么用计算机划因果图,计算机软考考试必备知识点:鱼骨图法
  13. 涉密台式计算机密码可以输入几次,涉密打印机、扫描仪等与涉密计算机之间不采用无线方式连接 - 作业在线问答...
  14. php升序排列,php关联数组怎么按键名实现升序排列
  15. 《深入理解计算机系统》实验二Bomb Lab下载和官方文档机翻
  16. 银河麒麟/ubuntu 下安装/卸载软件包命令大全
  17. 优秀课件笔记之视听巧记汉英成语2
  18. UG编程-适合新手的详细讲解
  19. 百度推广——搜索营销新视角(百度官方出品,俞敏洪、吴晓波、徐雷力荐!)
  20. 基于Java+MySQL实现(Web)高校资源综合发布系统【100010343】

热门文章

  1. 推荐几款实用性强的外业勘察地图软件
  2. (转)JSP详细教程
  3. 高一计算机信息基础课本内容,高中信息技术基础(必修)_教案
  4. Proe3.0-5.0安装说明
  5. Microsoft Office 不同电脑不同电脑登录用户的数据同步
  6. linux(计划任务)
  7. java字符串替换一部分_字符串中部分字符替换
  8. 收银机收款机USB通讯接口(341驱动) 可以用于客显 小票打印机
  9. 怎样在中国消灭IE6浏览器
  10. Eclipse启动Tomcat 警告: 基于APR的本地库加载失败.错误报告为