开心一刻
一天在路边看到一个街头采访
记者:请问,假如你儿子娶媳妇,给多少彩礼合适呢
大爷:一百万吧,再给一套房,一辆车
大爷沉思一下,继续说到:如果有能力的话再给老丈人配一辆车,毕竟他把女儿养这么大也不容易
记者:那你儿子多大了?
大爷:我没有儿子,有两个女儿
问题背景
最近生产环境出现了一个问题,错误日志类似如下
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
connect ,连接池最大创建 10 个 connect ,正在执行 sql 的 connect
connect ,那就等呗,一共等了 1010 毫秒,还是拿不到 connect ,就抛出 GetConnectionTimeoutException
connect
那有人就说了:连接池的最大数量设置大一点,问题不就解决了吗
最大连接数设置大一点只能说可以降低问题发生的概率,不能完全杜绝,因为网络情况、硬件资源的使用情况等等都是不稳定因素
今天要讲的不是连接池大小问题,而是超时设置问题,我们慢慢往下看
问题复现
我们先来模拟下上述问题
MySQL 版本: 5.7.21
Druid 版本: 1.1.12
spring-jdbc 版本: 5.2.3.RELEASE
DruidDataSource 初始化
为了方便演示,就手动初始化了
多线程查询
connect
模拟慢查询
connect
LOCK TABLES tbl_user WRITE
tbl_user 加上写锁,然后跑线程去查询 tbl_user
异常演示
先锁表,再启动程序
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
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
总结
Druid 的 removeAbandoned
removeAbandoned
Druid
配置的时候一定要弄清楚各个配置项的具体作业,不要去猜!
queryTimeout 即可在 DataSource 配置,也可在 JdbcTemplate