如何正確地獲取一個有效的數據庫連接

來源:https://www.iflym.com/index.php/code/201805160001.html

如何正確地獲取一個有效的數據庫連接

這幾天在研究各個數據庫連接池,比如ali druid, dbcp2 以及最近很火的連接池 HikariCP, 除了常規的池化連接對象管理外。關鍵區別就是如何創建連接,防止連接洩漏,如何獲取連接這些細節的區分點.在hikariCP的wiki中,提到一篇文章 https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down, 這裡面提到由於網絡的問題導致獲取連接會比預期的時間長的這一問題。這也是我們為什麼在選擇一些組件和框架時,均會優先使用偏新的版本的原因。一些老的,舊的連接池,因為api的限制,對一些極端情況的處理並不能如意。比如在測試中的c3p0,dbcp這些常規連接池,由於歷史原因,在處理一些新的需求時都不能滿足需求。

我們來看對於一個標準的數據庫連接,我們會有以下的要求:

  1. 在規定的時間內拿到數據庫連接
  2. 使用一個alive的connection進行數據訪問

整個要求其實就是判斷connection存活,處理超時的問題。在常規的數據訪問中,因為不同sql查詢的原因,我們不能夠期望數據庫查詢在一個較小的時間內就一定能返回(比如5秒)。但是在程序中,當獲取一個連接時,又期望在較小的時間內返回給應用,以方便應用能夠快速響應業務。如果5s之內(或者更小)不能拿到連接,能認為當前業務失敗,避免業務長時間不能響應。甚至避免整個系統所有線程均blocking在獲取數據庫連接這一步,而導致系統不可用.

那麼整個問題變為了以下3步:

  1. 業務在較小的時間內拿到連接,如果超時則立刻返回
  2. 連接池在較小的時間內創建數據庫連接,如果超時則立即返回,進行下一步嘗試
  3. 連接池在較小的時間內驗證已經創建好的連接當前可用,如果超時則立即返回,並標記當前連接不可用,選擇其它的連接


業務中獲取連接 maxWait

在業務中獲取連接對象時,即通過datasource.getConnection()來獲取,因為參數中無法傳遞額外的時間參數,一般是提前在相應的datasource實現中配置wait參數。比如在druid中可以配置maxWait參數,hikari可以配置connectionTimeout參數用於控制客戶端獲取連接的超時時間.

如果第一次數據源還沒有活躍的連接,則需要進行創建連接或者驗證已有的連接是否有效這些操作。一般情況下,這些流程可能會比maxWait的時間更長,如果採用阻塞式地操作,那麼這種情況下maxWait即沒有實際的作用。而導致實際上超過maxWait的時間情況下業務中還沒有響應結果的情況。

這種處理方式,可以採用,獲取連接和數據源創建連接兩個事務分開的作法,即通過2個線程,使用線程協作的方式來處理。具體處理方法參考如下

  1. 定義條件信號量signal
  2. 獲取連接線程處理,如果當前不能立即拿到連接(使用無等待邏輯), 則通知創建線程進行連接創建,並且信號條件condition.await(maxWait)
  3. 創建連接線程收到信號量,啟動創建連接的流程,如果能夠創建成功,則通過信號量反饋連接線程。signal.notify()
  4. 連接線程如果在maxWait時間內得到通知,則返回業務。否則,await將在 maxWait之後,喚醒線程。在這種情況下,業務線程將在最多maxWait時間之內即能夠響應業務.

線程協作,即是利用condition條件變量來完成業務的等待和喚醒處理,即一種將阻塞式調用更換為異步回調式調用。

創建數據庫連接 connectTimeout

數據庫連接池在獲取物理連接時,通過調用 Connection Driver.connect(String url, Properties info) 來創建連接。在這個接口中, 並沒有提供像連接超時的參數。不過對於像這個情況,針對於mysql數據庫,可以有以下兩種方式。

  1. 通過DriverManager.setLoginTimeout(int) 設置登陸超時時間。此方法設置的值,可以通過 DriverManager.getLoginTimeout() 來獲取。此獲取的值的具體使用則是由各個數據庫driver來決定的。 此方法設置可能無效…
  2. mysql數據庫連接參數中,可以在url或properties中配置參數, connectTimeout(毫秒值)。此參數的目的用於配置在mysql中連接數據庫的超時時間。

在當前的業務實現中,mysql會同時讀取connectTime和loginTimeout的參數值,並且以兩者之間的最小值為最終配置值.(如果只有1個配置,則用其中的一個).這裡面有1個細節,即是loginTimeout的配置值在mysql中只會讀取一次,因此必須在調用mysql創建連接時應該儘早地設置此參數。

在實際的下層實現中,最終的connectTimeout會綁定在物理連接上的socket連接對象上。在標準的java socket中,在連接時,可以通過 socket.connect(socketAddress address, int timeout) 來處理連接的問題,即最終將connectTimeout轉換為socket.connect時的timeout參數。 而如果沒有提供此值時,則默認為無限阻塞,具體阻塞的時長由操作系統來決定,這就不能保證了.

驗證數據庫連接 ValidationQueryTimeout

相應的connection被池化到datasource中的連接池中,當出現網絡錯誤或者其它問題導致實際的連接不可用時,在java中的connection其實是不能感知的。因此當從池中get一個connection時,需要進行再次驗證以保證這個連接是可用的。在相應的連接池框架中,均使用了類似testOnBorrow來處理。testOnBorrow則需要額外的validationQuery參數,以表示通過哪個查詢來驗證對象是否可用。

通常情況下,驗證連接是否可用,即使用此connection對象發一個最簡單的查詢語句,如果能夠拿到結果沒有報錯,則表示此連接就是有效的。

但是在此場景下,如果查詢很久沒有返回結果,則當前驗證查詢即被阻塞住,進而進一步阻塞業務。通過validatequeryTimeout參數可以處理此問題。此參數用於控制在驗證查詢時,相應的查詢超時時間,在相應的api實現上。可以有以下兩種方法來處理此問題

  1. 在進行查詢時使用PreparedStatement.setQueryTimeout 設置查詢超時時間.此參數控制每一次查詢時的超時時間
  2. 在相應的connection對象上,通過Connection.setNetworkTimeout(executor, timeout) 來設置網絡超時時間。此參數用於讀取數據時的超時時間。需要注意的是此參數從jdk1.7才提供,屬於jdbc 4.0中的一部分。

在具體的連接池實現上,ali druid使用第1種方案。而hikari則混合使用兩種方案,即優先使用第2種,如果不支持,則使用第1種。

在底層的實現上,最終的設置均反應到socket對象上的特定屬性soTimeout,即通過調用socket.setSoTimeout(timeout) 來實現此效果。當調用了此方法之後,後續的inputStream.read方法將受到此timeout的影響,當超過了timeout參數之後,相應的read即會throw SocketTimeoutException異常。而此api最終體現到tcp中的so_timeout參數.

需要注意的是,因此此參數是影響整個connection的,因此如果此參數設置之後,後續的其它業務查詢也會受影響,導致一些長時間的查詢直接報錯。因此,標準的處理方式,即先get到原來的舊時間設置,即通過 connection.getNetworkTimeout獲取舊值,在查詢了驗證查詢之後,再次調用connectin.setNetworkTimeout設置回舊值。 而如果在這個過程中有錯誤,則直接表示連接不可用,則不需要再設置舊值了.

mysql中的validation

由於標準的select 1 這種驗證查詢語句總是要走一次完整的發送,解析,查詢,返回結果,以及構建協議結果等。相對來說還是有點浪費資源,因此在mysql的版本中,mysql自動提供了一個更為簡單的協議語句,即ping指令。通過發送ping請求來探測網絡的狀態。相應的api為 ConnectionImpl.pingInternal(checkForClosedConnection, timeoutMillis) 此方法同樣接收timeout參數,同樣最終反應到socket的sotimeout中。此方法相對於mysql版本來說,是實現連接探測的更好選擇(相比query).

總結

在相應的數據庫連接池中,在開放的api中,開始逐漸提供更完善的網絡參數定義,以滿足更加精細地網絡需求。在開發與網絡相關的應用中,既然是簡單的數據庫連接操作,也需要關注這些信息。它關係到整個系統是否會受到阻塞式處理導致失去響應的情況,同時也是評價一個第三方組件是否合格的重要參數。而近來網絡開發越來越流行,瞭解這些下層的實現,也有助於在上層api受限的情況下,進一步滿足系統的需要,也提供一些封裝上層實現的參考。


分享到:


相關文章: