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
### 解决 Spring Boot 中 Hikari 连接池出现 Apparent Connection Leak Detected 异常 在应用程序中遇到 `Apparent connection leak detected` 的异常通常意味着存在未关闭的数据库连接。这不仅会浪费资源,还可能导致后续请求无法获得新的连接。 #### 诊断原因 此错误的根本原因是某些情况下未能正确释放从 Hikari 数据源获取到的 JDBC 连接对象[^2]。具体来说: - 当应用程序尝试获取一个新连接并执行 SQL 查询之后忘记调用 `close()` 方法来返回这个连接给连接池; - 或者是在发生异常的情况下没有适当地处理资源回收逻辑; 为了防止这种情况的发生,建议采用 try-with-resources 结构自动管理资源生命周期,确保即使发生了异常也能安全地关闭连接。 ```java try (Connection conn = dataSource.getConnection()) { // 执行查询操作... } catch (SQLException e) { logger.error("Error executing query", e); } ``` 上述代码片段展示了如何利用 Java7 及以上版本引入的 Try-With-Resources 特性简化资源管理和异常处理过程。 #### 配置调整 除了改进编码实践外,还可以考虑修改 HikariCP 的配置参数以增强其健壮性和容错能力: - 设置合理的最大等待时间和最小空闲连接数可以帮助减少因长时间占用而导致的泄漏风险。 ```yaml spring: datasource: hikari: maximumPoolSize: 10 # 最大连接数量 minimumIdle: 5 # 最小闲置连接数 idleTimeout: 600000 # 空闲超时时间(毫秒),默认值为10分钟 maxLifetime: 1800000 # 单个连接的最大存活期(毫秒), 默认30分钟 connectionTimeout: 30000 # 获取连接的最长时间(毫秒) ``` 此外,启用详细的日志记录有助于追踪潜在的问题源头。可以通过设置合适的 logging level 来捕获更多关于连接分配的信息。 ```properties logging.level.com.zaxxer.hikari=DEBUG ``` 这样可以在控制台或文件中查看有关每次借入/归还连接的具体情况,从而更容易发现哪里出现了问题[^1]。 #### 使用框架特性优化 对于基于 MyBatis Plus 构建的应用程序而言,确保选择了合适的数据源工厂类也很重要。例如,在多数据源场景下应避免使用 `SqlSessionFactoryBean` 而选择更兼容的方式如 `MybatisPlusAutoConfiguration` 提供的支持[^4]。 通过遵循最佳编程习惯以及合理配置 HikariCP 参数,可以有效降低甚至消除 "connection leak" 错误发生的可能性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值