1. 问题现象

间隔一段时间数据库连接就会偶尔超时的诡异现象。

The last packet successfully received from the server was 10,014 milliseconds ago. The last packet sent successfully to the server was 10,014 milliseconds ago.

2. 问题原因

2.1 数据库超时配置

在描述问题之前先说一下数据库,他有个比较关键的配置 SHOW VARIABLES LIKE 'wait_timeout';

wait_timeout

  1. 定义:wait_timeout 是控制非交互式会话(例如,来自应用程序或脚本的连接)的超时参数。它指示 MySQL 在断开非交互式会话连接之前等待的秒数

  2. 默认值:通常是 28800 秒(8 小时),但不同的 MySQL 版本可能有所不同。

  3. 应用场景:如果一个连接在设定的 wait_timeout 时间内未执行任何操作(没有任何 SQL 语句),MySQL 将自动关闭该连接。这个参数主要用于管理应用程序连接。

条件1:spring.datasource.druid.min-evictable-idle-time-millis 连接空闲1小时标记为可回收

条件2spring.datasource.druid.max-evictable-idle-time-millis 连接最大空闲3小时为强制回收

定时器:spring.datasource.druid.time-between-eviction-runs-millis 1 分钟执行一次定时任务,将所有连接都从连接池拿出来判断一下是否满足 条件1,条件2,假设全部有20个连接,运行一次检测到有10个连接已经空闲了1小时,他就将这10个连接标记为可回收,有5个连接检测到它已经空闲了3小时已经到了他的最大存活时间了必须要对它进行回收释放,这个时候druid就会将这个连接进行关闭。

重要的是数据库 wait_timeout 默认是28800 秒(8 小时),但我这里是1800 秒(30 分钟非常保守的一个时间,假设java服务启动然后druid初始化了20个连接放到连接池,1800 秒(30 分钟连接池的空闲连接不向我发出sql数据库就会将这个连接关闭,而druid还傻傻的等它空闲3小时才真正回收。实际这个连接早已经超过数据库的30分钟被它关闭掉了。等到java真的需要发sql了去druid连接池拿着一个已经被数据库关闭掉了的连接来发起请求那么意味着什么?那就是直接的报错了因为这个连接早已经不可用了。

2.2 监控慢超时sql

开启druid性能监控面板能看到当前服务发出的所有sql,里面能看到超时的sql针对性的对此sql作出优化,包括但不限于代码层面与数据库索引层面

3. 解决方案

3.1 druid适配数据库

在比较重要的系统中而且并发量不高可以开启预检测,这样在发出业务sql之前会执行一段 select 'x' 如果错误那么证明这个连接已经失效了,druid就会从连接池拿另外的来试验,直到 select 'x' 通过才会拿这个连接发出业务sql保证可靠性,但是这样在超高并发的情况下这样配置性能变得低下QPS性能说是会对半砍。

# 检测是否连接是否存活部分
spring.datasource.druid.validation-query=select 'x'
#单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
spring.datasource.druid.validation-query-timeout=1
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-borrow=true
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-return=true
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
spring.datasource.druid.test-while-idle=true

假设数据库的 wait_timeout 比较小 1800 秒(30 分钟,那么druid可以设置为 空闲5分钟标记会可回收,20分钟强制回收

3.2 数据库适配druid

如果是超高并发量的情况下,数据库的 wait_timeout 必须要设置的高一点,这样可以让druid连接池的空闲时间放的很大,这样不必担心连接会轻易被数据库释放掉,这样也不用开启预检测 select 'x' 功能QPS性能大幅上升,说到底还是要平衡这个数据库与连接池的超时配置保持最佳性能而又不失轻易断开连接。

显然的默认的数据库 28800 秒(8 小时),那么druid可以设置为 空闲4小时标记为可回收,6小时强制回收

4. 优化配置

# 最小5个
spring.datasource.druid.minIdle = 5
# 初始化10个
spring.datasource.druid.initialSize = 10
# 最大100个
spring.datasource.druid.maxActive = 100
spring.datasource.druid.filters = stat
spring.datasource.druid.maxPoolPreparedStatementPerConnectionSize = 20
# 连接保持空闲而不被驱逐的最小时间 (5分钟)
spring.datasource.druid.min-evictable-idle-time-millis = 300000
# 连接保持空闲而不被驱逐的最大时间  (10分钟)
spring.datasource.druid.max-evictable-idle-time-millis = 600000
# 1\空闲连接驱逐线程运行之间的时间间隔。即每隔多长时间,连接池会检查空闲连接的状态  (1分钟)
# 2\并驱逐空闲时间超过 min-evictable-idle-time-millis 或 max-evictable-idle-time-millis 的连接
spring.datasource.druid.time-between-eviction-runs-millis=60000

# 检测是否连接是否存活部分
spring.datasource.druid.validation-query=select 'x'
#单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
spring.datasource.druid.validation-query-timeout=1
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-borrow=true
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-return=true
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
spring.datasource.druid.test-while-idle=true

#开启druid性能监控面板
spring.datasource.druid.logAbandoned = true
spring.datasource.druid.removeAbandonedTimeout = 1800
spring.datasource.druid.removeAbandoned = true
spring.datasource.druid.stat-view-servlet.enabled = true
spring.datasource.druid.stat-view-servlet.url-pattern = /druid/*
spring.datasource.druid.stat-view-servlet.login-username = admin
spring.datasource.druid.stat-view-servlet.login-password = 123456
spring.datasource.druid.stat-view-servlet.reset-enable = true
spring.datasource.druid.web-stat-filter.enabled = true
spring.datasource.druid.web-stat-filter.url-pattern = /*
spring.datasource.druid.web-stat-filter.exclusions = *.js,*.gif,*.jpg,*.png,*.css,*.ico,*.jsp,/druid/*,/download/*
spring.datasource.druid.web-stat-filter.session-stat-enable = true
spring.datasource.druid.web-stat-filter.session-stat-max-count = 2000
spring.datasource.druid.web-stat-filter.profile-enable = true
spring.datasource.druid.web-stat-filter.principal-session-name = session_user_key
#允许任意网段访问性能监控面板
spring.datasource.druid.stat-view-servlet.allow =