错误分析、定位

在项目中使用到了数据库集群,使用时发现项目运行并且没有操作数据库一段时间之后再次操作数据库就会控制台出现以下报错。
在这里插入图片描述
在这里插入图片描述

Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@de1855 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.

项目整合mysql集群的时候使用的是苞米豆开发的动态数据源工具。

		<!--动态数据源,提供@DS注解方便使用动态数据源-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>2.5.4</version>
        </dependency>

看日志大概知道是连接池的错误,使用的是hikari数据库连接池。之前只知道druid没了解过hikari,在整合动态数据源工具的时候也没有配置相关的连接池。报错内容里建议我们使用小一些的maxLifetime值,所以很可能这个报错的原因是连接池的参数maxLifetime大于数据库的连接lifetime,连接池里的连接没过期,但数据库那边已经过期了,然后 operations after connection closed了。

mysql连接超时参数

mysql有关连接超时的参数有两个,一个是interactive_timeout 另一个是wait_timeout ,单位都是s
使用以下语句查看

show variables like '%timeout%';

在这里插入图片描述
这两个参数默认都是28800s,即8个小时;
其中interactive_timeout指的是mysql在关闭一个交互的连接之前所要等待的秒数 ;wait_timeout指的是mysql在关闭一个非交互的连接之前所要等待的秒数。通过mysql客户端连接数据库是交互式连接,通过jdbc连接数据库是非交互式连接。

HikariCP连接超时参数

Druid和HikariCP是处于活跃状态数据库连接池,据说更简单更快。但本文关注其连接超时参数。
在这里插入图片描述


查看源码以及官方文档得知
CONNECTION_TIMEOUT 是等待来自池的连接的最大毫秒数,默认30000ms,也就是30秒。至少是250 ms。
IDLE_TIMEOUT是连接允许在池中闲置的最长时间,默认600000ms,也就是10分钟。这个值至少是10 秒,0代表无限。这个值只在设置了minimumIdle 时才有效。
MAX_LIFETIME是池中连接最长生命周期,默认1800000 ms,也就是30分钟。这个值至少是30 秒,0代表无限。
这些值都远远小于mysql的8小时

官方文档对于maxlifetime提到

We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit

强烈建议我们将这个值设置为数据库连接超时的值短几秒。

超时时间的机制

长时间没有使用该Connection,Connection会被Mysql关闭(但不为null)。此时调用该Connection时就会抛出异常。
但是按道理来说,mysql那边8小时过期,而数据库连接池30分钟,不应该会出现这种问题的,并且这种问题出现在几十秒内。

使用命令查看目前的连接

SHOW PROCESSLIST;

在这里插入图片描述
发现存活着大量的连接,大约有三四十个,应该是之前程序启动留下来的一些连接,mysql这边并没有关闭连接,是连接池的问题。

那还是hikari的配置问题,只好按照提示试着改一下max-lifetime
如果没有使用动态连接库可以使用一下配置。

spring:
  datasource:
    hikari:
        max-lifetime: 300000

由于使用的是动态连接库,所以这个hikari要在其配置下配置。

spring:
  datasource:
    dynamic:
    	datasource:
			hikari:
		    	max-lifetime: 300000

改为800000在闲置五分钟后发送请求能够稳定触发报错,而当设置为30000时,则再也没出现错误了。此时mysql数据库的wait_timeout还是为8小时。
将wait_timeout改为了300秒,即和以上的修改一样,都是五分钟。修改之后大量的连接消失了,然后重启项目的时候发现启动时就有四个连接,他们的time值最大不超过300,超过了就会从0开始数起,这个值受max-lifetime制约,随着后面的请求次数增多,连接就增多,但最终固定在10个连接。这10个连接受的是hikari的最大线程数制约,由于没有设置闲置等待时间以及闲置线程数,所以大概这个程序运行期间一直都是10个连接,而当我退出程序之后,超过time超过300的连接会直接消失,不再从0开始数起。

这里有个非常绕的一点就是wait_timeout和interactive_timeout 。
mysql有两个级别的配置,一个是session,另一个是global。下买面这个语句默认是session级别:

show variables like '%timeout%';

如果要看mysql的global级别需要加global参数:

show global variables like '%timeout%';

这两个级别下一共有四个参数:

  • global级别的interactive_timeout
  • global级别的wait_timeout
  • session级别的interactive_timeout
  • session级别的wait_timeout

对于各个参数作用的详细实验过程可以看这个帖子,但是看得实在让人头晕。这里我总结一下,有一下几条需要注意的:

  1. 在交互模式下,session级别的 interactive_timeout 继承了global级别 interactive_timeout 。
  2. 在交互模式下,连接时长受interactive_timeout 影响。
  3. 在非交互模式下,session级别的wait_timeout ,继承了global级别wait_timeout 。
  4. 在非交互模式下,连接时长受wait_timeout 影响。
  5. 在交互模式下,session级别的wait_timeout ,继承了global级别interactive_timeout。
  6. global级别的值修改对后续的连接有效,session级别的值对当前连接有效。
  7. 相同等级下的参数互不影响。

有人一第五条得出结论:要修改wait_timeout还要同时修改global级别的interactive_timeout。
原因是他们一直查看和修改session级别的配置,但是这并没有什么本文来说没有什么意义。
我们的目的是修改后续我们的连接池的连接(非交互模式)的超时时间,也就是希望后续的session级别的wait_timeout的值是我们的设置的值。根据第三条规则可以知道,这时候我们设置global级别的wait_timeout就可以了。

set global wait_timeout=300;

了解清楚了mysql的timeout机制就不难明白,global级别的wait_timeout对应的就是maxlifetime,为了防止网络延迟导致maxlifetime没过期时发送请求,到达mysql时wait_timeout刚好过期,所以要将maxlifetime要设置的比maxlifetime小几秒。至于为何之前8小时的wait_timeout和30分钟的maxlifetime会在大概5分钟左右丢失连接,这个一时半会儿还弄不明白,先这么配置先吧。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐