記一次 Druid 超時(shí)配置的問(wèn)題?
點(diǎn)擊上方“Java金融”,選擇“設(shè)為星標(biāo)”
后臺(tái)回復(fù)"888"獲取bat面試題集
開(kāi)心一刻
一天在路邊看到一個(gè)街頭采訪
記者:請(qǐng)問(wèn),假如你兒子娶媳婦,給多少彩禮合適呢
大爺:一百萬(wàn)吧,再給一套房,一輛車
大爺沉思一下,繼續(xù)說(shuō)到:如果有能力的話再給老丈人配一輛車,畢竟他把女兒養(yǎng)這么大也不容易
記者:那你兒子多大了?
大爺:我沒(méi)有兒子,有兩個(gè)女兒

問(wèn)題背景
最近生產(chǎn)環(huán)境出現(xiàn)了一個(gè)問(wèn)題,錯(cuò)誤日志類似如下:
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 失敗,因?yàn)閺?Druid 連接池獲取 connection 超時(shí)了。

上圖的意思是:執(zhí)行 select * from tbl_user 之前,需要從 druid 連接池中獲取一個(gè) connect
而此時(shí)連接池的狀態(tài)是:一共 10 個(gè)激活的 connect ,連接池最大創(chuàng)建 10 個(gè) connect ,正在執(zhí)行 SQL 的 connect 也是 10 個(gè)。
所以不能創(chuàng)建新的 connect 那就等唄。一共等了 1010 毫秒,還是拿不到 connect ,就拋出 GetConnectionTimeoutException 異常。
簡(jiǎn)單點(diǎn)說(shuō)就是,連接池中連接數(shù)不夠,在規(guī)定的時(shí)間內(nèi)拿不到 connect。
那有人就說(shuō)了:連接池的最大數(shù)量設(shè)置大一點(diǎn),問(wèn)題不就解決了嗎?
最大連接數(shù)設(shè)置大一點(diǎn)只能降低問(wèn)題發(fā)生的概率,不能完全杜絕問(wèn)題。因?yàn)榫W(wǎng)絡(luò)條件、硬件資源的使用情況等等都是不確定因素。
今天要講的不是連接池大小問(wèn)題,而是超時(shí)設(shè)置問(wèn)題。我們慢慢往下看。
問(wèn)題復(fù)現(xiàn)
我們先來(lái)模擬下上述問(wèn)題:
MySQL 版本:5.7.21 ,隔離級(jí)別:RR
Druid 版本:1.1.12
spring-jdbc 版本:5.2.3.RELEASE
DruidDataSource 初始化
為了方便演示,就手動(dòng)初始化了:

多線程查詢
線程數(shù)多于連接池中 connect 數(shù)。

模擬慢查詢
如果查詢飛快,15 個(gè)查詢可能都用不上 10 個(gè) connect ,所以我們需要簡(jiǎn)單處理下。
很簡(jiǎn)單,給表加寫(xiě)鎖:
LOCK TABLES tbl_user WRITE給表 tbl_user 加上寫(xiě)鎖,然后跑線程去查詢 tbl_user 的數(shù)據(jù)。
異常演示
先鎖表,再啟動(dòng)程序

可以看到,15 個(gè)線程中,有 5 個(gè)線程獲取 connect 失?。?/span>
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_userThread-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_userThread-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_userThread-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_userThread-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
https://gitee.com/youzhibing/qsl-project/tree/master/druid-timeout
時(shí)間配置項(xiàng)
Druid 中關(guān)于時(shí)間的配置項(xiàng)有很多,我們我們重點(diǎn)來(lái)看下如下幾個(gè):
maxWait
最大等待時(shí)長(zhǎng),單位是毫秒,-1 表示無(wú)限制。
從連接池獲取 connect;
如果有空閑的 connect ,則直接獲取到;
如果沒(méi)有則最長(zhǎng)等待 maxWait 毫秒;
如果還獲取不到,則拋出 GetConnectionTimeoutException 異常。
removeAbandonedTimeout
設(shè)置 Druid 強(qiáng)制回收連接的時(shí)限,單位是秒。
從連接池獲取到 connect 開(kāi)始算起,超過(guò)此值后, Druid 將強(qiáng)制回收該連接。
官網(wǎng)也有說(shuō)明:連接泄漏監(jiān)測(cè)。
https://github.com/alibaba/druid/wiki/%E8%BF%9E%E6%8E%A5%E6%B3%84%E6%BC%8F%E7%9B%91%E6%B5%8B
validationQueryTimeout
檢測(cè)連接是否有效的超時(shí)時(shí)間,單位是秒,-1 表示無(wú)限制。
Druid 內(nèi)部的一個(gè)檢測(cè) connect 是否有效的超時(shí)時(shí)間,需要結(jié)合 validationQuery 來(lái)配置。
timeBetweenEvictionRunsMillis
檢查空閑連接的頻率,單位是毫秒, 非正整數(shù)表示不進(jìn)行檢查。
空閑連接檢查的間隔時(shí)間, Druid 池中的 connect 數(shù)量是一個(gè)動(dòng)態(tài)從 minIdle 到 maxActive 擴(kuò)張與收縮的過(guò)程。
connect 使用高峰期,數(shù)量會(huì)從 minIdle 擴(kuò)張到 maxActive 。使用低峰期, connect 數(shù)量會(huì)從 maxActive 收縮到 minIdle 。
收縮的過(guò)程會(huì)回收一些空閑的 connect ,而 timeBetweenEvictionRunsMillis 就是檢查空閑連接的間隔時(shí)間。
queryTimeout
執(zhí)行查詢的超時(shí)時(shí)間,單位是秒,-1 表示無(wú)限制。
最終會(huì)應(yīng)用到 Statement 對(duì)象上,執(zhí)行時(shí)如果超過(guò)此時(shí)間,則拋出 SQLException 。
transactionQueryTimeout
執(zhí)行一個(gè)事務(wù)的超時(shí)時(shí)間,單位是秒。
minEvictableIdleTimeMillis
最小空閑時(shí)間,單位是毫秒,默認(rèn) 30 分鐘。
如果連接池中非運(yùn)行中的連接數(shù)大于 minIdle ,并且某些連接的非運(yùn)行時(shí)間大于 minEvictableIdleTimeMillis ,則連接池會(huì)將這部分連接設(shè)置成 Idle 狀態(tài)并關(guān)閉。
maxEvictableIdleTimeMillis
最大空閑時(shí)間,單位是毫秒,默認(rèn) 7 小時(shí)。
如果 minIdle 設(shè)置的比較大,連接池中的空閑連接數(shù)一直沒(méi)有超過(guò) minIdle ,那么那些空閑連接是不是一直不用關(guān)閉?
當(dāng)然不是。
如果連接太久沒(méi)用,數(shù)據(jù)庫(kù)也會(huì)把它關(guān)閉(MySQL 默認(rèn) 8 小時(shí)),這時(shí)如果連接池不把這條連接關(guān)閉,程序就會(huì)拿到一條已經(jīng)被數(shù)據(jù)庫(kù)關(guān)閉的連接。
為了避免這種情況, Druid 會(huì)判斷池中的連接狀態(tài)。如果非運(yùn)行時(shí)間大于 maxEvictableIdleTimeMillis ,也會(huì)強(qiáng)行把它關(guān)閉,而不用判斷空閑連接數(shù)是否小于 minIdle 。
再看問(wèn)題
其實(shí)前面的示例中設(shè)置了。

獲取 connect 的最大等待時(shí)長(zhǎng)是 10000 毫秒,也就是 10 秒。
而 removeAbandonedTimeout 設(shè)置是 7 秒。
照理來(lái)說(shuō) connect 如果 7 秒未執(zhí)行完 SQL 查詢,就會(huì)被 Druid 強(qiáng)制回收進(jìn)連接池,那么等待 10 秒應(yīng)該能夠獲取到 connect 。
為什么會(huì)拋出 GetConnectionTimeoutException 異常了?
這也就是文章標(biāo)題中的超時(shí)設(shè)置問(wèn)題。
源碼探究
很顯然,我們從 dataSource.init(); 開(kāi)始跟源碼,會(huì)看到如下一段代碼:

我們繼續(xù)跟 createAndStartDestroyThread():

重點(diǎn)來(lái)了,我們看下 DestroyTask 到底是怎么樣一個(gè)邏輯:

我們接著跟進(jìn) removeAbandoned ,關(guān)鍵代碼:

如果 connect 正在運(yùn)行中是不會(huì)被強(qiáng)制回收進(jìn)連接池的。
回到我們的示例:connect 都是在運(yùn)行中,只是都在進(jìn)行慢查詢,所以是無(wú)法被強(qiáng)制回收進(jìn)連接池的,那么其他線程自然在 maxWait 時(shí)間內(nèi)無(wú)法獲取到 connect 。
至此,文章標(biāo)題中的問(wèn)題的原因就找到了。
那么問(wèn)題又來(lái)了:removeAbandonedTimeout 作用在哪?
我們?cè)僮屑?xì)閱讀下:連接泄漏監(jiān)測(cè)
https://github.com/alibaba/druid/wiki/%E8%BF%9E%E6%8E%A5%E6%B3%84%E6%BC%8F%E7%9B%91%E6%B5%8B
Druid 提供了 RemoveAbandanded 相關(guān)配置,目的是監(jiān)測(cè)連接泄露,回收那些長(zhǎng)時(shí)間游離在連接池之外的空閑 connect。
可能因?yàn)槌绦騿?wèn)題,導(dǎo)致申請(qǐng)的 connect 在處理完 SQL 查詢后,不能回到連接池的懷抱。那么這個(gè) connect 處理游離態(tài),它真實(shí)存在,但后續(xù)誰(shuí)也申請(qǐng)不到它,這就是連接泄露。
而 removeAbandoned 的設(shè)計(jì)就是為了幫助這些泄露的 connect 回到連接池的懷抱。
解決問(wèn)題
開(kāi)啟 removeAbandoned 對(duì)性能有影響,官方不建議在生產(chǎn)環(huán)境使用。
那么我們接受官方的建議,不開(kāi)啟 removeAbandoned (不配置即可,默認(rèn)是關(guān)閉的)。
為了不讓慢查詢占用整個(gè)連接池,而拖垮整個(gè)應(yīng)用,我們?cè)O(shè)置查詢超時(shí)時(shí)間 queryTimeout 。
有兩種方式,一個(gè)是設(shè)置 DataSource 的 queryTimeout ,另一個(gè)是設(shè)置 JdbcTemplate 的 queryTimeout 。
如果兩個(gè)都設(shè)置,最終生效的是哪個(gè),為什么?大家自己去分析,權(quán)當(dāng)是給大家留個(gè)一個(gè)作業(yè)。
這里就配置 DataSource 的 queryTimeout ,給大家演示下效果:

可以看到,所有線程都獲取到了 connect。
總結(jié)
Druid 的 removeAbandoned 對(duì)性能有影響,不建議開(kāi)啟。removeAbandoned 的開(kāi)啟后的作用要捋清楚,而非簡(jiǎn)單的過(guò)期強(qiáng)制回收;
Druid 的時(shí)間配置項(xiàng)有很多,不局限于文中所講,但常用的就那么幾個(gè),其他的保持默認(rèn)值就好。配置的時(shí)候一定要弄清楚各個(gè)配置項(xiàng)的具體作業(yè),不要去猜!
查詢超時(shí) queryTimeout 即可在 DataSource 配置,也可在 JdbcTemplate 配置。
轉(zhuǎn)自:青石路,
鏈接:cnblogs.com/youzhibing/p/16458860.html
推薦:
