一天在路邊看到一個街頭採訪
記者:請問,假如你兒子娶媳婦,給多少彩禮合適呢
大爺:一百萬吧,再給一套房,一輛車
大爺沉思一下,繼續說到:如果有能力的話再給老丈人配一輛車,畢竟他把女兒養這麼大也不容易
記者:那你兒子多大了?
大爺:我沒有兒子,有兩個女兒
上圖的意思是:執行 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
為了方便演示,就手動初始化了
執行緒數多於連線池中 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 中關於時間的設定項有很多,我們我們重點來看下如下幾個
最大等待時長,單位是毫秒,-1 表示無限制
從連線池獲取 connect ,如果有空閒的 connect ,則直接獲取到,如果沒有則最長等待 maxWait 毫秒,如果還獲取不到,則丟擲 GetConnectionTimeoutException 異常
設定 druid 強制回收連線的時限,單位是秒
從連線池獲取到 connect 開始算起,超過此值後, Druid 將強制回收該連線
官網也有說明:連線洩漏監測
檢測連線是否有效的超時時間,單位是秒,-1 表示無限制
Druid 內部的一個檢測 connect 是否有效的超時時間,需要結合 validationQuery 來設定
檢查空閒連線的頻率,單位是毫秒, 非正整數表示不進行檢查
空閒連線檢查的間隔時間, Druid 池中的 connect 數量是一個動態從 minIdle 到 maxActive 擴張與收縮的過程
connect 使用高峰期,數量會從 minIdle 擴張到 maxActive ,使用低峰期, connect 數量會從 maxActive 收縮到 minIdle
收縮的過程會回收一些空閒的 connect ,而 timeBetweenEvictionRunsMillis 就是檢查空閒連線的間隔時間
執行查詢的超時時間,單位是秒,-1 表示無限制
最終會應用到 Statement 物件上,執行時如果超過此時間,則丟擲 SQLException
執行一個事務的超時時間,單位是秒
最小空閒時間,單位是毫秒,預設 30 分鐘
如果連線池中非執行中的連線數大於 minIdle ,並且某些連線的非執行時間大於 minEvictableIdleTimeMillis ,則連線池會將這部分連線設定成 Idle 狀態並關閉
最大空閒時間,單位是毫秒,預設 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 設定