JDBC 4.2 Specifications 中文翻譯 -- 第十一章 鏈接池

在基本的 DataSource 實現中,客戶端的 Connection 對象與物理數據庫鏈接有着1:1的關係。當 Connection 被關閉之後,物理鏈接也會被關閉。所以,鏈接的頻繁打開、初始化以及關閉,會在一個客戶端會話中上演屢次,帶來了太重的性能消耗。
而鏈接池就能解決這個問題,鏈接池維護了一系列物理數據庫鏈接的緩存,能夠被多個客戶端會話重複使用。鏈接池可以極大地提升性能和可擴展性,特別是在一個三層架構的環境中,大量的客戶端能夠共享一個數量比較小的物理數據庫鏈接池。在圖11-1中,JDBC 驅動提供了一個 ConnectionPoolDataSource 的實現,應用服務器能夠用它來建立和管理鏈接池。
ConnectionPoolDataSource實現.png數據庫

鏈接池的管理策略跟具體的實現有關,也跟具體的應用服務器有關。應用服務器對客戶端提供了一個 DataSource 接口的具體實現,使得鏈接池化對於客戶端來講是透明的。最終,客戶端使用 DataSource API 就能和以前使用 JNDI 同樣,得到了更好的性能和可擴展性。緩存

下文將會介紹 ConnectionPoolDataSource 接口、PooledConnection 接口以及 ConnectionEvent 類,這三個組成部分是一個相互合做的關係,下文將以一個經典線程池的實現的角度,逐步描述這幾部分。這一章也會介紹基本的 DataSource 對象和池化的 DataSource 對象之間的區別,此外,還會討論一個池化的鏈接如何可以維護一堆可重用的 PreparedStatement 對象。服務器

儘管本章中的全部討論都是假設在三層架構環境下的,但鏈接的池化在兩層架構的環境下也一樣有用。
在兩層架構的環境中,JDBC 驅動既實現了 DataSource 接口,也實現 ConnectionPoolDataSource 接口,這種實現方式容許客戶端打開或者關閉多個鏈接。微信

11.1 ConnectionPoolDataSource 和 PooledConnection

通常來講, 一個 JDBC 驅動會去實現 ConnectionPoolDataSource 接口,應用服務器可使用這個接口來得到 PooledConnection 對象,如下代碼展現了 getPooledConnection 方法的兩種版本架構

public interface ConnectionPoolDataSource {
    PooledConnection getPooledConnection() throws SQLException;
    PooledConnection getPooledConnection(String user, String password) throws SQLException;
}

一個 PooledConnection 對象表明一條與數據源之間的物理鏈接。JDBC 驅動對於 PooledConnection 的實現,則會封裝全部與維護這條鏈接相關的細節。
應用服務器則會在它的 DataSource 接口的實現中,緩存和重用這些 PooledConnection。當客戶端調用 DataSource.getConnection 方法時,應用服務器將會使用物理 PooledConnection 去獲取一個邏輯 Connection 對象。如下代碼是 PooledConnection 接口的一些方法定義:app

public interface PooledConnection {
    Connection getConnection() throws SQLException;
    void close() throws SQLException;
    void addConnectionEventListener(
    ConnectionEventListener listener);
    void addStatementEventListener(
    StatementEventListener listener);
    void removeConnectionEventListener(
    ConnectionEventListener listener);
    void removeStatementEventListener(
    StatementEventListener listener);
}

當客戶端使用完鏈接之後,它使用 Connection.close 方法來關閉這條邏輯鏈接,這個動做只是關閉了邏輯鏈接,但並不會關閉物理鏈接。物理鏈接會被歸還到池子裏,以待重用。
在這裏,鏈接的池化對於客戶端來講徹底是透明的,客戶端能像使用非池化鏈接那樣去使用池化鏈接。工具

須要注意的是,當對池化的鏈接調用 Connection.close() 方法時,以前經過 Connection.setClientInfo 設置的屬性將會被清除掉。

11.2 鏈接事件

回憶先前說過的,當 Connection.close 方法被調用後,底層的物理鏈接 PooledConnection 就能夠再次被重用。當一個 PooledConnection 能夠被回收的時候,將會使用 JavaBean 風格的事件去通知鏈接池管理器(應用服務器)。
爲了發生鏈接事件時能被通知到,鏈接池管理器必須實現 ConnectionEventListener 接口,而後 PooledConnection 會將其註冊爲鏈接事件的一個監聽者。ConnectionEventListener 接口定義了兩個方法,也體現出了可能發生的兩種不一樣的事件:性能

  • connectionClosed 事件 --- 當邏輯鏈接 Connection.close 被調用時產生此事件
  • connectionErrorOccurred --- 當出現一些致命的錯誤,好比說數據庫宕機致使鏈接丟失的時候,會觸發這個事件

鏈接池管理器經過調用 PooledConnection.addConnectionEventListener 方法來將本身註冊爲一個 PooledConnection 的監聽者。通常狀況下,註冊的動做都發生在將鏈接歸還到池子裏以前。
JDBC 驅動負責在對應的事件發生的時候,調用回調方法,這兩個方法都須要一個 ConnectionEvent 對象做爲參數,經過這個對象能夠判斷究竟是哪一個 PooledConnection 被關閉了或者發生了錯誤。
當客戶端關閉了邏輯鏈接的時候,JDBC 驅動會經過調用監聽者所實現的 connectionClosed 方法來通知監聽者,此時,監聽者(鏈接池管理器)能夠將該鏈接歸還到池子裏以便重用。當致命性錯誤發生時,JDBC 驅動首先會調用監聽者實現的 connectionErrorOccurred 方法,而後再拋出一個 SQLException 異常。這個時候,監聽者就能夠經過 PooledConnection.close 方法來將物理鏈接關閉。優化

11.3 三層架構環境中的鏈接池化

如下步驟列出了客戶端使用鏈接池池化時,實際上發生的事情:spa

  • 客戶端調用 DataSource.getConnection 方法
  • 應用服務器在它本身支持鏈接池的 DataSource 實現中,查找是否有可用的 PooledConnection 對象
  • 若是沒有可用的 PooledConnection 對象,應用服務器調用 ConnectionPoolDataSource.getPooledConnection 來建立一條物理鏈接,JDBC 驅動的具體實現會負責鏈接建立的具體細節,並把它交給應用服務器管理。
  • 不管是新建的 PooledConnection 仍是已經建立好的處於可用狀態的,應用服務器會對這條鏈接進行一些標識,標記它處於正在使用的狀態。
  • 應用服務器調用 PooledConnection.getConnection 方法來得到一個邏輯上的 Connection 對象,這個對象底層實際上關聯了一個物理的 PooledConnection 對象,客戶端調用 DataSource.getConnection 方法,返回值拿到的是邏輯上的 Connection 對象。
  • 應用服務器經過調用 PooledConnection.addConnectionEventListener 方法將它本身註冊爲一個 ConnectionEventListener,當 PooledConnection 處於可用狀態時,應用服務器就會獲得相應的事件通知。
  • 由 DataSource.getConnection 方法返回的邏輯 Connection 對象,依然是使用 Connection API,直到 Connection.close 被調用以前,底層的 PooledConnection 都處於使用狀態,不可被重用。

即便在沒有應用服務器的兩層架構環境中,鏈接依然能夠作到池化。這種狀況下,JDBC 驅動須要實現 DataSource 接口和 ConnectionPoolDataSource 接口。

11.4 DataSource 實現與鏈接池化

拋開對性能和擴展性的提高不說,客戶端使用 DataSource 接口的時候,不須要去關心它底層的實現是否池化,客戶端面向的是一套統一的,無差異的使用方式。
常規的 DataSource 實現,即不實現鏈接池化功能的實現,通常由 JDBC 驅動實現,一般有如下兩個觀點被認爲是正確的:

  • DataSource.getConnection 方法建立一個新的 Connection 對象來表明一條真正的物理鏈接,而且封裝了全部維護和管理這條物理鏈接的細節。
  • Connection.close 方法關閉底層的物理鏈接並釋放相關的資源

在一個實現了池化的 DataSource 實現中,狀況則有些不同,如下幾個觀點被認爲是正確的:

  • 在 DataSource 的實現中,包含了一個提供了鏈接池化功能的模塊,這個模塊要怎麼實現沒有一個統一的標準,因人而異。這個模塊會緩存一系列 PooledConnection 對象。DataSource 的實現類,一般處於驅動實現的 ConnectionPoolDataSource 和 PooledConnection 接口的上層。
  • DataSource.getConnection 方法會調用 PooledConnection 方法去得到對底層物理鏈接的一個句柄,若是已有的鏈接池裏沒有現成可用的鏈接,那麼這個時候就須要新建物理鏈接,只有在這種狀況下,新建物理鏈接對性能的消耗才體現出來。當須要建立新的物理鏈接的時候,ConnectionPoolDataSource 的 getPooledConnection 會被調用,對於物理鏈接的管理細節,則委託給了 PooledConnection 對象。
  • Connection.close 方法被調用時,只是關閉邏輯上的鏈接句柄,並不會關閉實際上的物理鏈接。鏈接池管理者此時會收到一個事件通知,被告知一個 PooledConnection 處於可重用狀態了。若是此時客戶端仍然企圖使用這個邏輯上的鏈接句柄,那麼只會獲得一個 SQLException 異常。
  • 一個物理 PooledConnection 在它的整個生命週期中,可能會產生許多的邏輯 Connection 對象,但只有最近一次產生的 Connection 對象纔是有效的,當 PooledConnection.getConnection 方法被調用時,先前已經存在的 Connection 對象,將會被自動關閉。這種狀況下,關閉不會產生相應的事件去通知監聽者。
這給了應用服務器一種從客戶端強行拿走鏈接的方式,這種情形可能不多見,可是當應用服務器須要進行強制關閉時,這個特性可能會頗有用
  • 鏈接池的管理者經過調用 PooledConnection.close 方法來關閉物理鏈接,通常發生如下狀況時,纔會這麼作:當應用服務器正常退出時,當須要從新初始化鏈接的緩存時,或者是該鏈接上發生一些不可恢復的致命性錯誤時。

11.5 部署

進行鏈接池化的部署,須要提供一個客戶端代碼能夠接觸到的 DataSource 對象,而且還須要把一個 ConnectionPoolDataSource 對象註冊到 JNDI 中。
第一步,部署 ConnectionPoolDataSource,以下代碼所示:

// ConnectionPoolDS implements the ConnectionPoolDataSource
// interface. Create an instance and set properties.
com.acme.jdbc.ConnectionPoolDS cpds = new com.acme.jdbc.ConnectionPoolDS();
cpds.setServerName(「bookserver」);
cpds.setDatabaseName(「booklist」);
cpds.setPortNumber(9040);
cpds.setDescription(「Connection pooling for bookserver」);
// Register the ConnectionPoolDS with JNDI, using the logical name
// 「jdbc/pool/bookserver_pool」
Context ctx = new InitialContext();
ctx.bind(「jdbc/pool/bookserver_pool」, cpds);

上述步驟作好之後,ConnectionPoolDataSource 對象就能夠被對客戶端代碼可見的 DataSource 使用了,DataSource 的部署須要依賴於先前部署的 ConnectionPoolDataSource,以下代碼所示:

// PooledDataSource implements the DataSource interface.
// Create an instance and set properties.
com.acme.appserver.PooledDataSource ds =
new com.acme.appserver.PooledDataSource();
ds.setDescription(「Datasource with connection pooling」);
// Reference the previously registered ConnectionPoolDataSource
ds.setDataSourceName(「jdbc/pool/bookserver_pool」);
// Register the DataSource implementation with JNDI, using the logical
// name 「jdbc/bookserver」.
Context ctx = new InitialContext();
ctx.bind(「jdbc/bookserver」, ds);

到此,客戶端代碼就可使用這個 DataSource 了。

11.6 池化鏈接的 Statement 重用

JDBC 規範對於 statement 的池化也提供了一些支持。statement 池化這個特性,能讓應用層像 connection 重用同樣,對 PreparedStatement 進行重用,這個特性須要以鏈接池化爲基礎。
下圖展現了 PooledConnection 與 PreparedStament 之間的關係。邏輯 Connection 能夠透明地使用多個 PreparedStatement 對象。

statement池化.png

上圖中,鏈接池和 statement 池由應用服務器來實現。不過,這些功能其實也能夠由驅動來實現,或者是數據源來實現。這裏咱們對於 statement 池化的討論,實際上是適用於以上提到的全部實現方式的。

11.6.1 使用池化 Statement

對於 statement 的重用,必須對應用透明。也就是說,從應用開發的角度,對一個 statement 的使用,不須要關心它是不是池化的實現。statement 在底層會一直保持處於打開狀態,應用層的代碼也不須要改變。若是應用層關閉了這個 statement,它依然須要調用 Connection.prepareStatement 方法來繼續使用它。statement 的池化對於應用層來講,使用方式上是透明的,應用層惟一能感知到不一樣的,是它帶來的明顯的性能提高。
應用層須要經過調用 DatabaseMetadata 的 supportStatementPooling 方法,來判斷一個數據源是否支持 statement 重用。
在不少狀況下,對於 statement 的重用,是一種很是有意義的優化,尤爲是負責的 prepared statement。不過,須要注意的是,大量的 statement 處於打開狀態,有可能會對資源帶來影響。

11.6.2 關閉池化 Statement

一旦應用層關閉了一個 statement,不管它是不是池化的,它都不能再繼續被使用了,不然會致使異常拋出。
如下幾個方法會關閉一個池化的 statement:

  • Statement.close --- 由應用層調用。若是一個 statement 是池化的,調用這個方法會關閉邏輯上的 statement,但不會關閉底層的已經池化的物理 statement。
  • Connection.close --- 由應用層調用。

    • 非池化鏈接 --- 關閉底層的物理鏈接和由這個鏈接建立的全部 statement。這樣作是必要的,由於垃圾回收機制沒法檢測到外部的資源何時會被釋放。
    • 池化鏈接 --- 僅關閉邏輯上的鏈接和這個鏈接所建立的邏輯 statement,底層的物理鏈接以及相關的 statement 不會被關閉。
  • PooledConnection.close --- 由鏈接池管理者調用。會關閉底層的物理鏈接以及全部相關的 statement。

應用層沒法直接關閉一個已經池化的物理 statement,這是鏈接池管理器作的事情。PooledConnection.close 方法關閉物理鏈接以及全部的關聯 statement,釋放掉相關的資源。
應用層也沒法直接控制 statement 應該如何被池化。一個池化的 statement 老是與一個 PooledConnection 相關聯的,ConnectionPoolDataSource 能夠用來對池化作一些屬性設置。

11.7 statement 事件

若是鏈接池管理器支持 statement 池化,它必須實現 StatementEventListener 接口,而後將本身註冊爲 PooledConnection 對象的監聽者。這個接口定義瞭如下兩個方法,用來監聽有可能發生在一個 PreparedStatement 對象上的兩種事件。

  • statementClosed --- 當與 PooledConnection 對象相關聯的邏輯 PreparedStatement 對象被關閉時觸發,也就是說,當應用層調用 PreparedStatement.close 方法時。
  • statementErrorOccurred --- 當 JDBC 驅動監測到 PreparedStatement 對象不可用時觸發。

鏈接池管理器經過 PooledConnection.addStatementEventListener 方法將本身註冊爲監聽者。通常來講,在鏈接池管理器返回一個 PreparedStatement 對象給應用層使用以前,它必須先把本身註冊爲一個監聽者。
當對應的事件發生時,驅動會調用 StatementEventListener 的 statementClosed 方法和 statementErrorOccurred 方法,這兩個方法都接收一個 statementEvent 對象做爲參數,這個參數就能夠用來判斷是發生了關閉事件仍是異常事件。當 JDBC 應用關閉邏輯 statement ,或者一些錯誤發生時,JDBC 驅動會調用相關的方法,這個時候,鏈接池管理器它就能夠將這個 statement 放回池子以便重用,或者是拋出異常。

11.8 ConnectionPoolDataSource 屬性

JDBC 的 API 定義了一系列的屬性來設置與池化相關的屬性:

屬性名 類型 描述
maxStatements int 容許池化的最大 statement 數,0 表明不池化
initialPoolSize int 當鏈接池建立時須要建立的初始物理鏈接數
minPoolSize int 鏈接池最小物理鏈接數
maxPoolSize int 鏈接池最大物理鏈接數,0表明無限制
maxIdleTime int 鏈接空閒最大空閒時間,0表明無限制
propertyCycle int 屬性生效時間,單位爲秒

鏈接池的配置風格遵循 JavaBean 風格。鏈接池廠商若是須要增長配置屬性,那這些新增的屬性名不該與已有的標準屬性名重複。
與 DataSource 的實現同樣,ConnectionPoolDataSource 的實現也必須爲每一個屬性增長 setter 和 getter 方法,如下代碼是一個示例:

VendorConnectionPoolDS vcp = new VendorConnectionPoolDS();
vcp.setMaxStatements(25);
vcp.setInitialPoolSize(10);
vcp.setMinPoolSize(1);
vcp.setMaxPoolSize(0);
vcp.setMaxIdleTime(0);
vcp.setPropertyCycle(300);

應用服務器會根據設置的屬性,來決定應該如何管理相關的池子。
ConnectionPoolDataSource 的配置屬性無須被 JDBC 客戶端直接訪問。一些管理工具須要訪問的話,建議經過反射的方式。

掃一掃關注個人微信公衆號

相關文章
相關標籤/搜索