1、错误信息
com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@xxxxxxx (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value
在解决错误之前,先理解下HikariPool的相关配置内容:
1.1、认识HikariPool的结构
包括以下几个部分:
-
- ConcurrentBag & PoolEntry
-
- addConnectionExecutor
-
- houseKeeperTask
-
- HikariProxyConnection
-
- closeConnectionExecutor
1.2、ConcurrentBag
直译就是“连接口袋”,就是放数据库连接的口袋,也就是连接池。
ConcurrentBag有3个保存数据库连接的地方(池):
- threadList:是一个ThreadLocal变量,保存当前线程持有的数据库连接。
- sharedList:是一个CopyOnWriteArrayList,线程安全的arrayList,数据库连接创建后首先进入sharedList。
- handoffQueue:是一个SynchronousQueue,数据库连接创建、进入sharedList后,也会进入handoffQueue等待连接请求。
除此之外,ConcurrentBag还有一个比较重要的属性waiters,记录向连接池获取数据库连接而不得、等待的线程数。
1.3、PoolEntry
连接池中存储的对象不是Connection,也不是Connection的代理对象,而是PoolEntry。
PoolEntry持有数据库连接对象Connection,Connection在实例化PoolEntry前创建。PoolEntry创建的过程中会同时创建两个ScheduledFuture任务:endOfLife和keepalive。
endOfLife提交MaxLifetimeTask定时任务,在PoolEntry创建后的maxLifetime(参数指定)执行。MaxLifetimeTask会关闭当前连接、同时根据需要创建新的连接加入到连接池。
keepalive提交KeepaliveTask周期性任务,在PoolEntry创建后按照keepalive(参数设定)周期性执行,在当前连接空闲的情况下检查连接是否可用(使用参数指定的ConnectionTestQuery进行测试),如果连接不可用则关闭并重新创建连接。
1.4、addConnectionExecutor
添加数据库连接的任务管理器。负责数据库连接的创建以及加入到连接池中(sharedList和handoffQueue)。
注意addConnectionExecutor本身是一个线程池ThreadPoolExecutor,线程池容量为最大数据库连接数,HikariPool初始化的过程中会以多线程(corePoolSize=CPU内核数量)的方式快速完成连接创建和池化,之后正常情况下会以单线程(corePoolSize=1)的方式创建数据库连接。
houseKeeperTask
是一个周期性线程池ScheduledExecutorService,初始化的时候创建,定时(housekeepingPeriodMs)执行,清理掉(关闭)连接池中多余的(大于最小空闲数)空闲连接,如果连接池中的连接数没有达到参数设置的数量(最大连接数、或者空闲连接没有达到最小空闲连接数)则创建连接。
1.5、HikariProxyConnection
HikariProxyConnection是数据库连接Connection的扩展类,应用层通过getConnection方法获取到的数据库连接其实不是不是数据库连接Connection、而是这个扩展类HikariProxyConnection。
使用扩展类HikariProxyConnection、而不是原生的数据库连接Connection的一个最直观的理由是,对于数据库连接池来说,连接的关闭方法close不是要关闭连接、而是要把连接交还给连接池。使用扩展类(或者代理类)就可以很容易的重写其close方法实现目标,而如果直接使用原生Connection的话,就没办法控制了。
closeConnectionExecutor
数据库连接池需要关闭的时候,通过closeConnectionExecutor线程池提交,关闭连接后closeConnectionExecutor还负责调用fillPool(),根据需要填充连接池。
2、看下一般的配置
此错误提示看上去像是在说 maxLifetime 设置过长了,在HikariPool内部有一个规则,就是maxLifetime的设置时间要小于数据库的timeout,所以排查下maxLifetime和timeout的值
2.1、目前我的配置
- yml配置
datasource:
hikari:
max-lifetime: 1800000 # 设置为 30 分钟(比 MySQL 默认空闲超时时间短)
connection-test-query: SELECT 1
leak-detection-threshold: 2100 # 2秒检测连接泄露
connection-timeout: 30000 # 30秒的连接超时
idle-timeout: 600000 # 10分钟的空闲超时
- mysql配置 mysql有关连接超时的参数有两个,一个是interactive_timeout 另一个是wait_timeout ,单位都是s
show variables like '%timeout%';
Variable_name | value |
---|---|
interactive_timeout | 7200 |
wait_timeout | 7200 |
可以看出 max-lifetime 远小于 数据库的timeout时间,并且idle-timeout也小于max-lifetime配置
最终经过多方的源码翻看,原来需要再配置一个参数
datasource:
hikari:
keepalive-time: 509999
keepalive-time小于idle-timeout就行
3、总结
核心参数配置 数据库timeout > hikari.max-lifetime > hikari.idle-timeout > hikari.keepalive-time 完美解决问题
补充:4、Hikari连接池高性能的原因
-
1、采用自定义的FastList替代了ArrayList,FastList的get方法去除了范围检查rangeCheck逻辑,并且remove方法是从尾部开始扫描的,而并不是从头部开始扫描的。因为Connection的打开和关闭顺序通常是相反的
-
2、初始化时创建了两个HikariPool对象,一个采用final类型定义,避免在获取连接时才初始化,因为获取连接时才初始化就需要做同步处理
-
3、Hikari创建连接是通过javassist动态字节码生成技术创建的,性能更好
-
4、从连接池中获取连接时对于同一个线程在threadLocal中添加了缓存,同一线程获取连接时没有并发操作
-
5、Hikari最大的特点是在高并发的情况下尽量的减少锁竞争
评论