Spring Boot2.5 集成数据库连接池 HikariCP

摘要:浅谈连接池基本概念和工作原理、常见数据库连接池性能对比、HiKariCP速度为什么快和常见属性对比。最后给出一个Spring Boot整合HiKariCP的入门案例。

§工程环境

  • JDK:1.8.0_231
  • maven:3.6.1
  • Apache Tomcat:9.0.46
  • Spring Boot: 2.5.0
  • mysql-connector-java:8.0.25
  • mysql:8.0.25
  • HikariCP:4.0.3

§数据库连接池介绍

  一个普通的java程序,要查询数据库的数据,基本流程是这样的:

  可以看到:进行一次查询,要进行很多次网络交互,这样的缺点是:

  1. 网络IO多

  2. 响应时间长,导致QPS降低

  3. 频繁创建连接和关闭连接,浪费数据库资源,影响服务器性能

  因为TCP连接的创建开支十分昂贵,并且数据库所能承载的TCP并发连接数也有限制,针对这种场景,数据库连接池应运而生。数据库连接池是用于创建、管理和释放数据库连接的缓冲池技术,缓冲池中的连接可以被任何需要它们的线程使用。当一个线程需要用JDBC对一个数据库操作时,将从池中请求一个连接;当这个连接使用完毕后,将返回到连接池中,等待其它线程的调度。

  这里用到了池化技术,如大家屡见不鲜的线程池、整数池、字符串池、对象池和Http 连接池等等,都是对这个思想的应用。池化技术的思想主要是通过复用对象,以减少每次获取资源时创建和释放所带来的资源消耗,提高资源利用率,这是典型的以空间换取时间的策略。

  数据库连接池负责分配、管理、释放数据库连接,它允许应用服务重复使用数据库连接,而非重新建立。使用连接池之后,流程是这样的:

  由此可见,数据库连接的创建和关闭连接均由连接池来实现。这样的机制有如下两个优点:

  1. 封装关于数据库访问的各种参数,实现统一管理
  2. 通过对数据库的连接池管理,减少网络开销并提升数据库性能

数据库连接池工作原理剖析

  数据库连接池的工作原理主要由三部分组成,分别为

  • 连接池的建立
  • 连接池的管理
  • 连接池的关闭
  1. 连接池的建立。应用初始化时,根据配置的最小连接数,在连接池将创建此数目的数据库连接放到池中,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,这样避免了连接随意建立和关闭造成的系统开销。

    Java中提供了很多容器类可以方便的构建连接池,例如Vector、Stack等。

  2. 连接池的管理。连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。其管理策略是:当客户请求数据库连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,如果没达到就重新创建一个连接给请求的客户;如果达到就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。 当客户释放数据库连接时,先判断池中的连接数是否超过了设置的最大连接数,如果超过就从连接池中删除该连接;否则,保留连接,等待再次使用。

  3. 连接池的关闭。应用程序关闭时,关闭连接池中所有连接,释放所有相关资源。

  在Java这个自由开放的生态中,已经有非常多优秀的开源数据库连接池可以供大家选择,比如:DBCP、C3P0、Druid、HikariCP、tomcat-jdbc等。而在Spring Boot 2.x中,对数据源的选择也紧跟潮流,采用了目前性能最佳的HikariCP。接下来,我们就来具体聊聊HikariCP。

§Java常见数据库连接池性能比较

  单从性能角度分析,性能从高到低依次是:HikariCP、druid、tomcat-jdbc、dbcp、c3p0。下图是HikariCP官网给出的性能对比:

img

  从上图中可以直观的看出,Hikari 在 获取和释放 Connection 和 Statement 方法的 OPS 不是一般的高,那是相当的高,基本上是碾压其他连接池,这里就不一一点名了。除了 OPS 外,HikariCP 的稳定性也更好,性能毛刺更少。

§数据库连接池选型 Druid vs HikariCP性能对比

  • 从功能角度考虑,Druid 功能更丰富,除具备连接池基本功能外,还支持sql级监控、扩展、SQL防注入等。最新版甚至有集群监控。两者的侧重点不一样。
  • 从性能角度考虑,从数据处理速度角度来看,HikariCP确实更强,但Druid由阿里巴巴背书,可支持”双十一”等最严苛的使用场景,并且提供了强大的监控功能,在国内有不少用户。不过,Spring Boot 2.x已经使用HikariCP作为默认的数据库连接池,其优秀程度可见一斑。
  • 从监控角度考虑,如果我们有像skywalking、prometheus等组件是可以将监控能力交给这些的,HikariCP也可以将metrics暴露出去。

  HikariCP作为后起之秀,是目前最快的Java数据库连接池。

§HikariCP为什么这么快

  HikariCP为什么这么快呢?是因为它在如下四个方面做了优化,以提升性能:

  1. 优化并精简字节码。使用Java字节码修改类库Javassist来生成委托实现动态代理,比JDK Proxy生成的字节码更少,精简了很多不必要的字节码。
  2. 使用自定义的无锁的、性能更好的并发集合类ConcurrentBag。
  3. 使用自定义的数组类型FastList替代ArrayList。FastList是List接口的精简实现。
  4. 优化代理和拦截器:减少代码,例如 HikariCP 的 Statement proxy 只有100行代码,只有 BoneCP 的十分之一;

  下面是FastList源码:

/**
 * ArrayList精简版的、没有列表检查的 FastList
 *
 * @author Brett Wooldridge
 */
public final class FastList<T> implements List<T>, RandomAccess, Serializable{
  private static final long serialVersionUID = -4598088075242913858L;

  private final Class<?> clazz;
  private T[] elementData;
  private int size;

  /**
   * 构建一个默认大小为32的列表。
   * @param clazz the Class stored in the collection
   */
  @SuppressWarnings("unchecked")
  public FastList(Class<?> clazz) {
     this.elementData = (T[]) Array.newInstance(clazz, 32);
     this.clazz = clazz;
  }

  /**
   * 构造具有指定大小的列表。
   * @param clazz the Class stored in the collection
   * @param capacity the initial size of the FastList
   */
  @SuppressWarnings("unchecked")
  public FastList(Class<?> clazz, int capacity) {
     this.elementData = (T[]) Array.newInstance(clazz, capacity);
     this.clazz = clazz;
  }

@Override
   public boolean add(T element) {
      //给 list添加属性
      //如果 size值小于 初始化的值
      if (size < elementData.length) {
         elementData[size++] = element;
      } else {
         // 溢出的代码
         //elementData 原始32不够用 需要扩容
         final int oldCapacity = elementData.length;
         final int newCapacity = oldCapacity << 1;
         @SuppressWarnings("unchecked")
         //扩容集合
         final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity);
         //数组复制
         System.arraycopy(elementData, 0, newElementData, 0, oldCapacity);
         //属性赋值
         newElementData[size++] = element;
         elementData = newElementData;
      }

      return true;
   }
   /**
    * 贴出ArrayList的get代码,来看看为什么 FastList 更快
    *  public E get(int index) {
    *   rangeCheck(index);
    *    return elementData(index);
    *   }
    *   ArrayList调用rangeCheck以检查角标范围,而FastList直接读取元素,节约时间
    */
   @Override
   public T get(int index) {
      return elementData[index];
   }
   /**
    * 这个是ArrayList的 remove()代码, FastList 少了检查范围和从头到尾的 检查元素动作,速度更快
    *   rangeCheck(index);
    *   modCount++;
    *    E oldValue = elementData(index);
    */
   @Override
   public boolean remove(Object element) {
      for (int index = size - 1; index >= 0; index--) {
         if (element == elementData[index]) {
            final int numMoved = size - index - 1;
            //如果角标不是最后一个 copy一个新的数组结构
            if (numMoved > 0) {
               System.arraycopy(elementData, index + 1, elementData, index, numMoved);
            }
            //如果角标是最后面的 直接初始化为null
            elementData[--size] = null;
            return true;
         }
      }
      return false;
   }

§数据源配置详解

  由于Spring Boot的自动化配置机制,大部分对于数据源的配置都可以通过配置参数的方式去改变。只有一些特殊情况,比如:更换默认数据源,多数据源共存等情况才需要去修改覆盖初始化的Bean内容。本节我们主要讲Hikari的配置,所以对于使用其他数据源或者多数据源的情况,在之后的教程中学习。

  在Spring Boot自动化配置中,对于数据源的配置可以分为两类:

  • 通用配置:以spring.datasource.*的形式存在,主要是对一些即使使用不同数据源也都需要配置的一些常规内容。比如:数据库链接地址、用户名、密码等。这里就不做过多说明了,通常就这些配置:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

温馨提示:driver-class-name用于指定JDBC驱动程序的类名,默认从jdbc url中自动探测。

com.mysql.jdbc.Driver 是 mysql-connector-java 5中的,com.mysql.cj.jdbc.Driver 是 mysql-connector-java 版本6以后的。

  • 数据源连接池配置:以spring.datasource.<数据源名称>.*的形式存在,比如:Hikari的配置参数就是spring.datasource.hikari.*形式。下面这个是我们最常用的几个配置项及对应说明:
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=500000
spring.datasource.hikari.max-lifetime=540000
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.connection-test-query=SELECT 1

  这些配置的含义:

  • spring.datasource.hikari.minimum-idle: 最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size。

  • spring.datasource.hikari.maximum-pool-size: 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值

  • spring.datasource.hikari.idle-timeout: 空闲连接超时时间,此属性控制允许连接在连接池中闲置的最长时间。默认值600000毫秒(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。

    此设置仅适用于maximumPoolSize-minimumIdle的连接。一旦连接池达到最小连接数,空闲连接将不会退出。在超时之前,连接永远不会退出。值为0意味着空闲连接永远不会从池中删除。允许的最小值是10000ms(10秒),默认值值是600000(10分钟)。

  • spring.datasource.hikari.max-lifetime: 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短

  • spring.datasource.hikari.connection-timeout: 连接超时时间:毫秒,小于250ms,否则被重置为默认值30秒

  • spring.datasource.hikari.connection-test-query: 用于测试连接是否可用的查询语句

更多完整配置项可查看下表:

name 默认配置validate之后的值 构造器默认值 validate重置 描述
autoCommit TRUE TRUE – 自动提交从池中返回的连接
connectionTimeout 30000 SECONDS.toMillis(30) = 30000 如果小于250毫秒,则被重置回30秒 等待来自池的连接的最大毫秒数
idleTimeout 600000 MINUTES.toMillis(10) = 600000 如果idleTimeout+1秒>maxLifetime 且 maxLifetime>0,则会被重置为0(代表永远不会退出);如果idleTimeout!=0且小于10秒,则会被重置为10秒 连接允许在池中闲置的最长时间
maxLifetime 1800000 MINUTES.toMillis(30) = 1800000 如果不等于0且小于30秒则会被重置回30分钟 池中连接最长生命周期
connectionTestQuery null null – 如果您的驱动程序支持JDBC4,我们强烈建议您不要设置此属性
minimumIdle 10 -1 minIdle<0或者minIdle>maxPoolSize,则被重置为maxPoolSize 池中维护的最小空闲连接数
maximumPoolSize 10 -1 如果maxPoolSize小于1,则会被重置。当minIdle<=0被重置为DEFAULT_POOL_SIZE则为10;如果minIdle>0则重置为minIdle的值 池中最大连接数,包括闲置和使用中的连接
metricRegistry null null – 该属性允许您指定一个 Codahale / Dropwizard MetricRegistry 的实例,供池使用以记录各种指标
healthCheckRegistry null null – 该属性允许您指定池使用的Codahale / Dropwizard HealthCheckRegistry的实例来报告当前健康信息
poolName HikariPool-1 null – 连接池的用户定义名称,主要出现在日志记录和JMX管理控制台中以识别池和池配置
initializationFailTimeout 1 1 – 如果池无法成功初始化连接,则此属性控制池是否将 fail fast
isolateInternalQueries FALSE FALSE – 是否在其自己的事务中隔离内部池查询,例如连接活动测试
allowPoolSuspension FALSE FALSE – 控制池是否可以通过JMX暂停和恢复
readOnly FALSE FALSE – 从池中获取的连接是否默认处于只读模式
registerMbeans FALSE FALSE – 是否注册JMX管理Bean(MBeans)
catalog null driver default – 为支持 catalog 概念的数据库设置默认 catalog
connectionInitSql null null – 该属性设置一个SQL语句,在将每个新连接创建后,将其添加到池中之前执行该语句。
driverClassName null null – HikariCP将尝试通过仅基于jdbcUrl的DriverManager解析驱动程序,但对于一些较旧的驱动程序,还必须指定driverClassName
transactionIsolation null null – 控制从池返回的连接的默认事务隔离级别
validationTimeout 5000 SECONDS.toMillis(5) = 5000 如果小于250毫秒,则会被重置回5秒 连接将被测试活动的最大时间量
leakDetectionThreshold 0 0 如果大于0且不是单元测试,则进一步判断:(leakDetectionThreshold < SECONDS.toMillis(2) or (leakDetectionThreshold > maxLifetime && maxLifetime > 0),会被重置为0 . 即如果要生效则必须>0,而且不能小于2秒,而且当maxLifetime > 0时不能大于maxLifetime 记录消息之前连接可能离开池的时间量,表示可能的连接泄漏
dataSource null null – 这个属性允许你直接设置数据源的实例被池包装,而不是让HikariCP通过反射来构造它
schema null driver default – 该属性为支持模式概念的数据库设置默认模式
threadFactory null null – 此属性允许您设置将用于创建池使用的所有线程的java.util.concurrent.ThreadFactory的实例。
scheduledExecutor null null – 此属性允许您设置将用于各种内部计划任务的java.util.concurrent.ScheduledExecutorService实例

§数据源配置案例

  数据库连接池properties文件配置信息:

###数据源配置###

#默认就是hikari,可缺省
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root

#默认30000ms,即30s
#spring.datasource.hikari.connection-timeout=30000
#存活时间,默认600000ms,即10min
spring.datasource.hikari.idle-timeout=600000
#连接池的最大尺寸(闲置连接+正在使用的连接),默认10
spring.datasource.hikari.maximum-pool-size=200
#最小空闲连接数,默认10
spring.datasource.hikari.minimum-idle=50
spring.datasource.hikari.pool-name=私有连接池

  HikariDataSource在应用启动后,第一次数据库交互的时候加载连接池信息,这就是因为Spring Boot 2.x连接数据库用到了懒加载。

§Reference

posted @ 2021-06-18 17:35  楼兰胡杨  阅读(3661)  评论(0)    收藏  举报