最近生产环境出现了一个问题,错误日志类似如下
Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 1010, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
日志信息提示的很明显:获取
JDBC Connection
失败,因为从
druid
连接池获取
connection
超时了
上图的意思是:执行
select * from tbl_user
之前,需要从
druid
连接池中获取一个
connect
而此时连接池的状态是:一共 10 个激活的
connect
,连接池最大创建 10 个
connect
,正在执行 sql 的
connect
也是 10 个
所以不能创建新的
connect
,那就等呗,一共等了
1010
毫秒,还是拿不到
connect
,就抛出
GetConnectionTimeoutException
异常
简单点说就是是连接池中连接数不够,在规定的时间内拿不到
connect
那有人就说了:连接池的最大数量设置大一点,问题不就解决了吗
最大连接数设置大一点只能说可以降低问题发生的概率,不能完全杜绝,因为网络情况、硬件资源的使用情况等等都是不稳定因素
今天要讲的不是连接池大小问题,而是超时设置问题,我们慢慢往下看
我们先来模拟下上述问题
MySQL
版本:
5.7.21
,隔离级别:RR
Druid
版本:
1.1.12
spring-jdbc
版本:
5.2.3.RELEASE
DruidDataSource 初始化
为了方便演示,就手动初始化了
多线程查询
线程数多于连接池中
connect
数
模拟慢查询
如果查询飞快,15 个查询,可能都用不上 10 个
connect
,所以我们需要简单处理下
很简单,给表加写锁呗:
LOCK TABLES tbl_user WRITE
给表
tbl_user
加上写锁,然后跑线程去查询
tbl_user
的数据
先锁表,再启动程序
可以看到,15 个线程中,有 5 个线程获取
connect
失败
Thread-13 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-5 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-10 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-7 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
Thread-8 Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10004, active 10, maxActive 10, creating 0, runningSqlCount 10 : select * from tbl_user
示例代码:
druid-timeout
时间配置项
Druid
中关于时间的配置项有很多,我们我们重点来看下如下几个
maxWait
最大等待时长,单位是毫秒,-1 表示无限制
从连接池获取
connect
,如果有空闲的
connect
,则直接获取到,如果没有则最长等待
maxWait
毫秒,如果还获取不到,则抛出
GetConnectionTimeoutException
异常
removeAbandonedTimeout
设置 druid 强制回收连接的时限,单位是秒
从连接池获取到
connect
开始算起,超过此值后,
Druid
将强制回收该连接
官网也有说明:
连接泄漏监测
validationQueryTimeout
检测连接是否有效的超时时间,单位是秒,-1 表示无限制
Druid
内部的一个检测
connect
是否有效的超时时间,需要结合
validationQuery
来配置
timeBetweenEvictionRunsMillis
检查空闲连接的频率,单位是毫秒, 非正整数表示不进行检查
空闲连接检查的间隔时间,
Druid
池中的
connect
数量是一个动态从
minIdle
到
maxActive
扩张与收缩的过程
connect
使用高峰期,数量会从
minIdle
扩张到
maxActive
,使用低峰期,
connect
数量会从
maxActive
收缩到
minIdle
收缩的过程会回收一些空闲的
connect
,而
timeBetweenEvictionRunsMillis
就是检查空闲连接的间隔时间
queryTimeout
执行查询的超时时间,单位是秒,-1 表示无限制
最终会应用到
Statement
对象上,执行时如果超过此时间,则抛出
SQLException
transactionQueryTimeout
执行一个事务的超时时间,单位是秒
minEvictableIdleTimeMillis
最小空闲时间,单位是毫秒,默认 30 分钟
如果连接池中非运行中的连接数大于
minIdle
,并且某些连接的非运行时间大于
minEvictableIdleTimeMillis
,则连接池会将这部分连接设置成
Idle
状态并关闭
maxEvictableIdleTimeMillis
最大空闲时间,单位是毫秒,默认 7 小时
如果
minIdle
设置的比较大,连接池中的空闲连接数一直没有超过
minIdle
,那么那些空闲连接是不是一直不用关闭?
当然不是,如果连接太久没用,数据库也会把它关闭(MySQL 默认 8 小时),这时如果连接池不把这条连接关闭,程序就会拿到一条已经被数据库关闭的连接
为了避免这种情况,
Druid
会判断池中的连接,如果非运行时间大于
maxEvictableIdleTimeMillis
,也会强行把它关闭,而不用判断空闲连接数是否小于
minIdle
其实前面的示例中设置了
获取
connect
的最大等待时长是
10000
毫秒,也就是
10
秒
而
removeAbandonedTimeout
设置是 7 秒
照理来说
connect
如果 7 秒未执行完
SQL
查询,就会被
Druid
强制回收进连接池,那么等待
10
秒应该能够获取到
connect
,为什么会抛出
GetConnectionTimeoutException
异常了?
这也就是文章标题中的超时设置问题
很显然,我们从
dataSource.init();
开始跟源码
会看到如下一块代码
我们继续跟
createAndStartDestroyThread();
重点来了,我们看下
DestroyTask
到底是怎么样一个逻辑
我们接着跟进
removeAbandoned
,关键代码
如果
connect
正在运行中是不会被强制回收进连接池的
回到我们的示例,
connect
都是在运行中,只是都在进行慢查询,所以是无法被强制回收进连接池的,那么其他线程自然在
maxWait
时间内无法获取到
connect
至此文章标题中的问题的原因就找到了
那么问题又来了:
removeAbandonedTimeout
作用在哪?
我们再仔细阅读下:
连接泄漏监测
Druid
提供了
RemoveAbandanded
相关配置,目的是监测连接泄露,回收那些长时间游离在连接池之外的空闲
connect
可能因为程序问题,导致申请的
connect
在处理完
sql
查询后,不能回到连接池的怀抱,那么这个
connect
处理游离态,它真实存在,但后续谁也申请不到它,这就是连接泄露
而
removeAbandoned
的设计就是为了帮助这些泄露的
connect
回到连接池的怀抱
开启
removeAbandoned
对性能有影响,官方不建议在生产环境使用
那么我们接受官方的建议,不开启
removeAbandoned
(不配置即可,默认是关闭的)
为了不让慢查询占用整个连接池,而拖垮整个应用,我们设置查询超时时间
queryTimeout
有两种方式,一个是设置
DataSource
的
queryTimeout
,另一个是设置
JdbcTemplate
的
queryTimeout
如果两个都设置,最终生效的是哪个,为什么
?大家自己去分析,权当是给大家留个一个作业
这里就配置
DataSource
的
queryTimeout
,给大家演示下效果
可以看到,所有线程都获取到了
connect
1、
Druid
的
removeAbandoned
对性能有影响,不建议开启
removeAbandoned
的开启后的作用要捋清楚,而非简单的过期强制回收
2、
Druid
的时间配置项有很多,不局限于文中所讲,但常用的就那么几个,其他的保持默认值就好
配置的时候一定要弄清楚各个配置项的具体作业,不要去猜!
3、查询超时
queryTimeout
即可在
DataSource
配置,也可在
JdbcTemplate
配置