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  楼兰胡杨  阅读(3291)  评论(0编辑  收藏  举报