摘要:浅谈连接池基本概念和工作原理、常见数据库连接池性能对比、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程序,要查询数据库的数据,基本流程是这样的:
??可以看到:进行一次查询,要进行很多次网络交互,这样的缺点是:
-
网络IO多
-
响应时间长,导致QPS降低
-
频繁创建连接和关闭连接,浪费数据库资源,影响服务器性能
??因为TCP连接的创建开支十分昂贵,并且数据库所能承载的TCP并发连接数也有限制,针对这种场景,数据库连接池应运而生。数据库连接池是用于创建、管理和释放数据库连接的缓冲池技术,缓冲池中的连接可以被任何需要它们的线程使用。当一个线程需要用JDBC对一个数据库操作时,将从池中请求一个连接;当这个连接使用完毕后,将返回到连接池中,等待其它线程的调度。
??这里用到了池化技术,如大家屡见不鲜的线程池、整数池、字符串池、对象池和Http 连接池等等,都是对这个思想的应用。池化技术的思想主要是通过复用对象,以减少每次获取资源时创建和释放所带来的资源消耗,提高资源利用率,这是典型的以空间换取时间的策略。
??数据库连接池负责分配、管理、释放数据库连接,它允许应用服务重复使用数据库连接,而非重新建立。使用连接池之后,流程是这样的:
??由此可见,数据库连接的创建和关闭连接均由连接池来实现。这样的机制有如下两个优点:
- 封装关于数据库访问的各种参数,实现统一管理
- 通过对数据库的连接池管理,减少网络开销并提升数据库性能
数据库连接池工作原理剖析
??数据库连接池的工作原理主要由三部分组成,分别为
- 连接池的建立
- 连接池的管理
- 连接池的关闭
-
连接池的建立。应用初始化时,根据配置的最小连接数,在连接池将创建此数目的数据库连接放到池中,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,这样避免了连接随意建立和关闭造成的系统开销。
Java中提供了很多容器类可以方便的构建连接池,例如Vector、Stack等。
-
连接池的管理。连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。其管理策略是:当客户请求数据库连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,如果没达到就重新创建一个连接给请求的客户;如果达到就按设定的最大等待时间进行等待,如果超出最大等待时间,则抛出异常给客户。 当客户释放数据库连接时,先判断池中的连接数是否超过了设置的最大连接数,如果超过就从连接池中删除该连接;否则,保留连接,等待再次使用。
-
连接池的关闭。应用程序关闭时,关闭连接池中所有连接,释放所有相关资源。
??在Java这个*开放的生态中,已经有非常多优秀的开源数据库连接池可以供大家选择,比如:DBCP、C3P0、Druid、HikariCP、tomcat-jdbc等。而在Spring Boot 2.x中,对数据源的选择也紧跟潮流,采用了目前性能最佳的HikariCP。接下来,我们就来具体聊聊HikariCP。
§Java常见数据库连接池性能比较
??单从性能角度分析,性能从高到低依次是:HikariCP、druid、tomcat-jdbc、dbcp、c3p0。下图是HikariCP官网给出的性能对比:
??从上图中可以直观的看出,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为什么这么快呢?是因为它在如下四个方面做了优化,以提升性能:
- 优化并精简字节码。使用Java字节码修改类库Javassist来生成委托实现动态代理,比JDK Proxy生成的字节码更少,精简了很多不必要的字节码。
- 使用自定义的无锁的、性能更好的并发集合类ConcurrentBag。
- 使用自定义的数组类型FastList替代ArrayList。FastList是List接口的精简实现。
- 优化代理和拦截器:减少代码,例如 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连接数据库用到了懒加载。