在基本的 DataSource
實現中,客戶端的 Connection 對象與物理數據庫鏈接有着1:1的關係。當 Connection 被關閉之後,物理鏈接也會被關閉。所以,鏈接的頻繁打開、初始化以及關閉,會在一個客戶端會話中上演屢次,帶來了太重的性能消耗。
而鏈接池就能解決這個問題,鏈接池維護了一系列物理數據庫鏈接的緩存,能夠被多個客戶端會話重複使用。鏈接池可以極大地提升性能和可擴展性,特別是在一個三層架構的環境中,大量的客戶端能夠共享一個數量比較小的物理數據庫鏈接池。在圖11-1中,JDBC 驅動提供了一個 ConnectionPoolDataSource 的實現,應用服務器能夠用它來建立和管理鏈接池。數據庫
鏈接池的管理策略跟具體的實現有關,也跟具體的應用服務器有關。應用服務器對客戶端提供了一個 DataSource 接口的具體實現,使得鏈接池化對於客戶端來講是透明的。最終,客戶端使用 DataSource API 就能和以前使用 JNDI 同樣,得到了更好的性能和可擴展性。緩存
下文將會介紹 ConnectionPoolDataSource 接口、PooledConnection 接口以及 ConnectionEvent 類,這三個組成部分是一個相互合做的關係,下文將以一個經典線程池的實現的角度,逐步描述這幾部分。這一章也會介紹基本的 DataSource 對象和池化的 DataSource 對象之間的區別,此外,還會討論一個池化的鏈接如何可以維護一堆可重用的 PreparedStatement 對象。服務器
儘管本章中的全部討論都是假設在三層架構環境下的,但鏈接的池化在兩層架構的環境下也一樣有用。
在兩層架構的環境中,JDBC 驅動既實現了 DataSource 接口,也實現 ConnectionPoolDataSource 接口,這種實現方式容許客戶端打開或者關閉多個鏈接。微信
通常來講, 一個 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 設置的屬性將會被清除掉。
回憶先前說過的,當 Connection.close 方法被調用後,底層的物理鏈接 PooledConnection 就能夠再次被重用。當一個 PooledConnection 能夠被回收的時候,將會使用 JavaBean 風格的事件去通知鏈接池管理器(應用服務器)。
爲了發生鏈接事件時能被通知到,鏈接池管理器必須實現 ConnectionEventListener 接口,而後 PooledConnection 會將其註冊爲鏈接事件的一個監聽者。ConnectionEventListener 接口定義了兩個方法,也體現出了可能發生的兩種不一樣的事件:性能
鏈接池管理器經過調用 PooledConnection.addConnectionEventListener 方法來將本身註冊爲一個 PooledConnection 的監聽者。通常狀況下,註冊的動做都發生在將鏈接歸還到池子裏以前。
JDBC 驅動負責在對應的事件發生的時候,調用回調方法,這兩個方法都須要一個 ConnectionEvent 對象做爲參數,經過這個對象能夠判斷究竟是哪一個 PooledConnection 被關閉了或者發生了錯誤。
當客戶端關閉了邏輯鏈接的時候,JDBC 驅動會經過調用監聽者所實現的 connectionClosed 方法來通知監聽者,此時,監聽者(鏈接池管理器)能夠將該鏈接歸還到池子裏以便重用。當致命性錯誤發生時,JDBC 驅動首先會調用監聽者實現的 connectionErrorOccurred 方法,而後再拋出一個 SQLException 異常。這個時候,監聽者就能夠經過 PooledConnection.close 方法來將物理鏈接關閉。優化
如下步驟列出了客戶端使用鏈接池池化時,實際上發生的事情:spa
即便在沒有應用服務器的兩層架構環境中,鏈接依然能夠作到池化。這種狀況下,JDBC 驅動須要實現 DataSource 接口和 ConnectionPoolDataSource 接口。
拋開對性能和擴展性的提高不說,客戶端使用 DataSource 接口的時候,不須要去關心它底層的實現是否池化,客戶端面向的是一套統一的,無差異的使用方式。
常規的 DataSource 實現,即不實現鏈接池化功能的實現,通常由 JDBC 驅動實現,一般有如下兩個觀點被認爲是正確的:
在一個實現了池化的 DataSource 實現中,狀況則有些不同,如下幾個觀點被認爲是正確的:
這給了應用服務器一種從客戶端強行拿走鏈接的方式,這種情形可能不多見,可是當應用服務器須要進行強制關閉時,這個特性可能會頗有用
進行鏈接池化的部署,須要提供一個客戶端代碼能夠接觸到的 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 了。
JDBC 規範對於 statement 的池化也提供了一些支持。statement 池化這個特性,能讓應用層像 connection 重用同樣,對 PreparedStatement 進行重用,這個特性須要以鏈接池化爲基礎。
下圖展現了 PooledConnection 與 PreparedStament 之間的關係。邏輯 Connection 能夠透明地使用多個 PreparedStatement 對象。
上圖中,鏈接池和 statement 池由應用服務器來實現。不過,這些功能其實也能夠由驅動來實現,或者是數據源來實現。這裏咱們對於 statement 池化的討論,實際上是適用於以上提到的全部實現方式的。
對於 statement 的重用,必須對應用透明。也就是說,從應用開發的角度,對一個 statement 的使用,不須要關心它是不是池化的實現。statement 在底層會一直保持處於打開狀態,應用層的代碼也不須要改變。若是應用層關閉了這個 statement,它依然須要調用 Connection.prepareStatement 方法來繼續使用它。statement 的池化對於應用層來講,使用方式上是透明的,應用層惟一能感知到不一樣的,是它帶來的明顯的性能提高。
應用層須要經過調用 DatabaseMetadata 的 supportStatementPooling 方法,來判斷一個數據源是否支持 statement 重用。
在不少狀況下,對於 statement 的重用,是一種很是有意義的優化,尤爲是負責的 prepared statement。不過,須要注意的是,大量的 statement 處於打開狀態,有可能會對資源帶來影響。
一旦應用層關閉了一個 statement,不管它是不是池化的,它都不能再繼續被使用了,不然會致使異常拋出。
如下幾個方法會關閉一個池化的 statement:
Connection.close --- 由應用層調用。
應用層沒法直接關閉一個已經池化的物理 statement,這是鏈接池管理器作的事情。PooledConnection.close 方法關閉物理鏈接以及全部的關聯 statement,釋放掉相關的資源。
應用層也沒法直接控制 statement 應該如何被池化。一個池化的 statement 老是與一個 PooledConnection 相關聯的,ConnectionPoolDataSource 能夠用來對池化作一些屬性設置。
若是鏈接池管理器支持 statement 池化,它必須實現 StatementEventListener 接口,而後將本身註冊爲 PooledConnection 對象的監聽者。這個接口定義瞭如下兩個方法,用來監聽有可能發生在一個 PreparedStatement 對象上的兩種事件。
鏈接池管理器經過 PooledConnection.addStatementEventListener 方法將本身註冊爲監聽者。通常來講,在鏈接池管理器返回一個 PreparedStatement 對象給應用層使用以前,它必須先把本身註冊爲一個監聽者。
當對應的事件發生時,驅動會調用 StatementEventListener 的 statementClosed 方法和 statementErrorOccurred 方法,這兩個方法都接收一個 statementEvent 對象做爲參數,這個參數就能夠用來判斷是發生了關閉事件仍是異常事件。當 JDBC 應用關閉邏輯 statement ,或者一些錯誤發生時,JDBC 驅動會調用相關的方法,這個時候,鏈接池管理器它就能夠將這個 statement 放回池子以便重用,或者是拋出異常。
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 客戶端直接訪問。一些管理工具須要訪問的話,建議經過反射的方式。