開心一刻
一天在路邊看到一個街頭採訪
記者:請問,假如你兒子娶媳婦,給多少彩禮合適呢
大爺:一百萬吧,再給一套房,一輛車
大爺沉思一下,繼續說到:如果有能力的話再給老丈人配一輛車,畢竟他把女兒養這麼大也不容易
記者:那你兒子多大了?
大爺:我沒有兒子,有兩個女兒
問題背景
最近生產環境出現了一個問題,錯誤日誌類似如下
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_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
時間配置項
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
配置
作者:青石路