Hikari maxLifetime 问题记录

引入

最近线上服务报了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原理
  1. 成员变量
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

结论:

  1. 使用try-with-resources语法无论是否抛出异常在try-block执行完毕后都会调用资源的close方法。
  2. 使用try-with-resources语法创建多个资源,try-block执行完毕后调用的close方法的顺序与创建资源顺序相反。
  3. 使用try-with-resources语法,try-block块抛出异常后先执行所有资源(try的()中声明的)的close方法然后在执行catch里面的代码然后才是finally。
  4. try的()中管理的资源在构造时抛出的异常,需要在本try对应的catch中捕获。
  5. 自动调用的close方法显示声明的异常,需要在本try对应的catch中捕获。
  6. 当你的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
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值