引入
最近线上服务报了Hikari连接告警的问题,告警日志如下:
2023-05-30 12:39:11.587 WARN — [ery-engine[T#4]] com.zaxxer.hikari.pool.PoolBase : HikariPool-3 - Failed to validate connection com.mysql.jdbc.JDBC4Connection@6e2d668f (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
问题分析
首先根据告警提示,我们可以看出是maxLifetime属性配置的问题,那maxLifetime属性是什么作用呢?
此属性控制池中连接的最大生存期。使用中的连接永远不会停止使用,只有在关闭连接后才将其删除。在逐个连接的基础上,应用较小的负衰减以避免池中的质量消灭。 我们强烈建议设置此值,它应该比任何数据库或基础结构施加的连接时间限制短几秒钟。 值0表示没有最大寿命(无限寿命),当然要遵守该idleTimeout设置。最小允许值为30000ms(30秒)。 默认值:1800000(30分钟)
问题应用服务上hikari采用的是默认配置,所以最大空闲时间是30分钟。
通过在源码中对告警日志进行搜索,我们知道上述告警产生的原因是hikari探活的时候发现当前MySQL连接关闭了,但是hikari保持的连接还在生命周期内;
MYSQL连接的生命周期(连接空闲时间)是8个小时,这远远大于hikari的30分钟,那这是为什么呢?
后来咨询了DBA后,公司使用的JED库(一种弹性MYSQL库)每10分钟会kill一次连接,那这就是问题的原因了。
show variables like '%timeout%';
interactive_timeout(交互式连接) 28800
wait_timeout(非交互式JDBC连接) 28800
问题解决
通过上面的分析,我们知道原因是因为连接在生命周期内被非正常关闭了,这时我们只需要调整hikari连接的生命周期比MYSQL连接时间短一些即可。
spring.datasource.hikari.max-lifetime=540000 //连接最大生命周期需要比MYSQL连接空闲时间10分钟短一些
spring.datasource.hikari.idle-timeout=480000 //连接空闲时间需要比最大生命周期时间短一些
Hikari常用参数配置参考
# 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
spring.datasource.hikari.connection-timeout=30000
# 最小连接数
spring.datasource.hikari.minimum-idle=5
# 最大连接数
spring.datasource.hikari.maximum-pool-size=15
# 自动提交
spring.datasource.hikari.auto-commit=true
# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),默认:10分钟
spring.datasource.hikari.idle-timeout=600000
# 连接池名字
spring.datasource.hikari.pool-name=xxxHikariCP
# 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms,建议设置比数据库超时时长少60秒,参考MySQL wait_timeout参数(show variables like '%timeout%';) -->
spring.datasource.hikari.max-lifetime=28740000
spring.datasource.hikari.connection-test-query=SELECT 1
延伸
Hikari源码阅读
HikariDataSource
public HikariDataSource(HikariConfig configuration) {
//参数核验及参数初始化
configuration.validate();
//将hikariConfig中的属性值copy到HikariDataSource
configuration.copyStateTo(this);
LOGGER.info("{} - Starting...", configuration.getPoolName());
// 初始化HikariPool
this.pool = this.fastPathPool = new HikariPool(this);
LOGGER.info("{} - Start completed.", configuration.getPoolName());
this.seal();
}
ConCurrentBag原理
- 成员变量
public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
//保存容器内所有元素
private final CopyOnWriteArrayList<T> sharedList;
//threadList中的元素是否使用WeakReference保存,对于弱引用对象,GC时会被收回
private final boolean weakThreadLocals;
//当前线程持有的元素,可以理解为sharedList的子集
private final ThreadLocal<List<Object>> threadList;
//通知外部往ConcurrentBag加入元素
private final IBagStateListener listener;
//当前等待获取元素的线程数量
private final AtomicInteger waiters;
//标记ConcurrentBag是否关闭,默认false。当关闭后ConcurrentBag无法添加新元素
private volatile boolean closed;
//阻塞队列,交接队列
private final SynchronousQueue<T> handoffQueue;
}
AutoCloseable使用
简介:
实现AutoCloseable接口可以使用try-with-resources语法糖
使用实例:
public class AutoCloseableTest implements AutoCloseable{
public String hello(){
// System.out.println(1/0);
return "hello world";
}
@Override
public void close() throws Exception {
System.out.println("here is close!");
}
@Test
public void test(){
try(AutoCloseableTest autoCloseableTest = new AutoCloseableTest()) {
System.out.println(autoCloseableTest.hello());
} catch (Exception e) {
System.out.println("exception cause is "+e.getCause());
e.printStackTrace();
}finally {
System.out.println("here is finally");
}
}
}
日志一:
hello world
here is close!
here is finally
日志二:主动抛出异常System.out.println(1/0);
here is close!
exception cause is null
java.lang.ArithmeticException: / by zero
at com.jd.sop.hikari.sync.AutoCloseableTest.hello(AutoCloseableTest.java:11)
at com.jd.sop.hikari.sync.AutoCloseableTest.test(AutoCloseableTest.java:23)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
here is finally
结论:
- 使用try-with-resources语法无论是否抛出异常在try-block执行完毕后都会调用资源的close方法。
- 使用try-with-resources语法创建多个资源,try-block执行完毕后调用的close方法的顺序与创建资源顺序相反。
- 使用try-with-resources语法,try-block块抛出异常后先执行所有资源(try的()中声明的)的close方法然后在执行catch里面的代码然后才是finally。
- try的()中管理的资源在构造时抛出的异常,需要在本try对应的catch中捕获。
- 自动调用的close方法显示声明的异常,需要在本try对应的catch中捕获。
- 当你的try-catch-finally分别在try/catch/finally中有异常抛出,而无法抉择抛出哪个异常时,你应该抛出try中对应的异常,并通过Throwable.addSuppressed方式记录它们间的关系。
CopyOnWriteArrayList使用
简介:
CopyOnWriteArrayList是一种能不加锁同时读写的容器,主要的思路是当我们对容器内进行写入操作时,不直接的操作原容器信息,而是copy出来一个新的容器,对新容器进行写操作,最后将原容器的引用指向新容器;
好处:保证了读写的并发性
坏处:用这个容器存储占内存的大对象时,会存在内存占用过高,频繁GC的问题;不能保证数据的实时一致性;
使用示例:
public class CopyOnWriteArrayListTest {
private final CopyOnWriteArrayList<Double> sharedList = new CopyOnWriteArrayList<>();
@Test
public void test01(){
new Thread(() ->{
while (true){
final double random = Math.random();
System.out.println("write element: "+random);
try {
sharedList.add(random);
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
try {
System.out.println("read sharedList size: "+sharedList.size());
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
while (true){
}
}
}
通过日志我们可以看出,写入与读取有不一致的情况:
read sharedList size: 0
write element: 0.45585596625496383
read sharedList size: 1
write element: 0.22535361228241213
read sharedList size: 2
write element: 0.08788451120075536
read sharedList size: 3
write element: 0.726907446421342
read sharedList size: 4
write element: 0.08897992480384831
read sharedList size: 5
read sharedList size: 5
write element: 0.42890370594919813
write element: 0.348389682893836
read sharedList size: 6 //这里应该是7个元素了
简单看一下写入的源码:
//容器中的数组用volatile修饰保证了内存可见性
private transient volatile Object[] array;
public boolean add(E e) {
//加锁
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
//容器中的数组指向了新创建的数组
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
SynchronousQueue阻塞队列
简介:
SynchronousQueue阻塞队列或者叫交换队列,无存储功能,在存储与取出元素时需要同时或者阻塞一段时间才能成功,因为是阻塞的,所以内部有两种排队形式,一种是公平模块的前进先出;一种是非公平模式的后进先出;
使用示例:
public class SynchronousQueueTest {
private final SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
@Test
public void test01(){
new Thread(() ->{
while (true){
final double random = Math.random();
System.out.println("produce element: "+random);
try {
synchronousQueue.offer(""+random,3, TimeUnit.SECONDS);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
try {
final String poll = synchronousQueue.poll(5,TimeUnit.SECONDS);
System.out.println("consume element: "+poll);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
while (true){
}
}
}
通过输出日志可以看出,生产与消费是成对出现的
produce element: 0.6245956944234445
consume element: 0.6245956944234445
produce element: 0.49804659471973967
consume element: 0.49804659471973967
参考文档
- https://juejin.cn/post/6887371883810357255
- https://juejin.cn/post/6884846863917268999#heading-8