[數據庫連接池] Java數據庫連接池--DBCP淺析.
前言
對於數據庫連接池, 想必大家都已經不再陌生, 這裏僅僅設計Java中的兩個常用數據庫連接池: DBCP和C3P0(後續會更新).
一. 為何要使用數據庫連接池
假設網站一天有很大的訪問量,數據庫服務器就需要為每次連接創建一次數據庫連接,極大的浪費數據庫的資源,並且極易造成數據庫服務器內存溢出、拓機。
數據庫連接是一種關鍵的有限的昂貴的資源,這一點在多用戶的網頁應用程序中體現的尤為突出.對數據庫連接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響到程序的性能指標.數據庫連接池正式針對這個問題提出來的.數據庫連接池負責分配,管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是重新建立一個
數據庫連接池在初始化時將創建一定數量的數據庫連接放到連接池中, 這些數據庫連接的數量是由最小數據庫連接數來設定的.無論這些數據庫連接是否被使用,連接池都將一直保證至少擁有這麽多的連接數量.連接池的最大數據庫連接數量限定了這個連接池能占有的最大連接數,當應用程序向連接池請求的連接數超過最大連接數量時,這些請求將被加入到等待隊列中.
數據庫連接池的最小連接數和最大連接數的設置要考慮到以下幾個因素:
1, 最小連接數:是連接池一直保持的數據庫連接,所以如果應用程序對數據庫連接的使用量不大,將會有大量的數據庫連接資源被浪費.
2, 最大連接數:是連接池能申請的最大連接數,如果數據庫連接請求超過次數,後面的數據庫連接請求將被加入到等待隊列中,這會影響以後的數據庫操作
3, 如果最小連接數與最大連接數相差很大:那麽最先連接請求將會獲利,之後超過最小連接數量的連接請求等價於建立一個新的數據庫連接.不過,這些大於最小連接數的數據庫連接在使用完不會馬上被釋放,他將被 放到連接池中等待重復使用或是空間超時後被釋放.
二, 數據庫連接池的原理及實現
1, 建立一個數據庫連接池pool, 池中有若幹個Connection 對象, 當用戶發來請求需要進行數據庫交互時則會使用池中第一個Connection對象.
2, 當本次連接結束時, 再將這個Connection對象歸還池中, 這樣就可以保證池中一直有足夠的Connection對象.
public class SimplePoolDemo { //創建一個連接池 private static LinkedList<Connection> pool = new LinkedList<Connection>(); //初始化10個連接 static{ try { for (int i = 0; i < 10; i++) { Connection conn = DBUtils.getConnection();//得到一個連接 pool.add(conn); } } catch (Exception e) { throw new ExceptionInInitializerError("數據庫連接失敗,請檢查配置"); } } //從池中獲取一個連接 public static Connection getConnectionFromPool(){ return pool.removeFirst();//移除一個連接對象 } //釋放資源 public static void release(Connection conn){ pool.addLast(conn); } }
以上的Demo就是一個簡單的數據庫連接池的例子, 先在靜態代碼塊中初始化10個Connection對象, 當本次請求結束後再將Connection添加進池中.
這只是我們自己手動去實現的, 當然在實際生產中並不需要我們去手動去寫數據庫連接池. 下面就重點講DBCP和C3P0的實現方式.
三, DBCP連接池
首先我們來看DBCP 的例子, 然後根據例子來分析:
DBCPUtils:
1 public class DBCPUtils { 2 private static DataSource ds;//定義一個連接池對象 3 static{ 4 try { 5 Properties pro = new Properties(); 6 pro.load(DBCPUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties")); 7 ds = BasicDataSourceFactory.createDataSource(pro);//得到一個連接池對象 8 } catch (Exception e) { 9 throw new ExceptionInInitializerError("初始化連接錯誤,請檢查配置文件!"); 10 } 11 } 12 //從池中獲取一個連接 13 public static Connection getConnection() throws SQLException{ 14 return ds.getConnection(); 15 } 16 17 public static void closeAll(ResultSet rs,Statement stmt,Connection conn){ 18 if(rs!=null){ 19 try { 20 rs.close(); 21 } catch (SQLException e) { 22 e.printStackTrace(); 23 } 24 } 25 26 if(stmt!=null){ 27 try { 28 stmt.close(); 29 } catch (SQLException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 if(conn!=null){ 35 try { 36 conn.close();//關閉 37 } catch (SQLException e) { 38 e.printStackTrace(); 39 } 40 } 41 } 42 }
在這個closeAll方法中, conn.close(); 這個地方會將connection還回到池子中嗎? DataSource 中是如何處理close()方法的呢?
上面的兩個問題就讓我們一起來看看源碼是如何來實現的吧.
這裏我們從ds.getConnection();入手, 看看一個數據源DataSource是如何創建connection的.
用eclipse導入:commons-dbcp-1.4-src.zip和commons-pool-1.5.6-src.zip則可查看源碼:
BasicDataSource.class:(implements DataSource)
public Connection getConnection() throws SQLException { return createDataSource().getConnection(); }
3.1 接下來看createDataSoruce() 方法:
1 protected synchronized DataSource createDataSource() 2 throws SQLException { 3 if (closed) { 4 throw new SQLException("Data source is closed"); 5 } 6 7 // Return the pool if we have already created it 8 if (dataSource != null) { 9 return (dataSource); 10 } 11 12 // create factory which returns raw physical connections 13 ConnectionFactory driverConnectionFactory = createConnectionFactory(); 14 15 // create a pool for our connections 16 createConnectionPool(); 17 18 // Set up statement pool, if desired 19 GenericKeyedObjectPoolFactory statementPoolFactory = null; 20 if (isPoolPreparedStatements()) { 21 statementPoolFactory = new GenericKeyedObjectPoolFactory(null, 22 -1, // unlimited maxActive (per key) 23 GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL, 24 0, // maxWait 25 1, // maxIdle (per key) 26 maxOpenPreparedStatements); 27 } 28 29 // Set up the poolable connection factory 30 createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig); 31 32 // Create and return the pooling data source to manage the connections 33 createDataSourceInstance(); 34 35 try { 36 for (int i = 0 ; i < initialSize ; i++) { 37 connectionPool.addObject(); 38 } 39 } catch (Exception e) { 40 throw new SQLNestedException("Error preloading the connection pool", e); 41 } 42 43 return dataSource; 44 }
從源代碼可以看出,createDataSource()方法通過7步,逐步構造出一個數據源,下面是詳細的步驟:
1、檢查數據源是否關閉或者是否創建完成,如果關閉了就拋異常,如果已經創建完成就直接返回。
2、調用createConnectionFactory()創建JDBC連接工廠driverConnectionFactory,這個工廠使用數據庫驅動來創建最底層的JDBC連接
3、調用createConnectionPool()創建數據源使用的連接池,連接池顧名思義就是緩存JDBC連接的地方。
4、如果需要就設置statement的緩存池,這個一般不需要設置
5、調用createPoolableConnectionFactory創建PoolableConnection的工廠,這個工廠使用上述driverConnectionFactory來創建底層JDBC連接,然後包裝出一個PoolableConnection,這個PoolableConnection與連接池設置了一對多的關系,也就是說,連接池中存在多個PoolableConnection,每個PoolableConnection都關聯同一個連接池,這樣的好處是便於該表PoolableConnection的close方法的行為,具體會在後面詳細分析。
6、調用createDataSourceInstance()創建內部數據源
7、為連接池中添加PoolableConnection
經過以上7步,一個數據源就形成了,這裏明確一點,一個數據源本質就是連接池+連接+管理策略。下面,將對每一步做詳細的分析。
3.2 JDBC連接工廠driverConnectionFactory的創建過程
1 protected ConnectionFactory createConnectionFactory() throws SQLException { 2 // Load the JDBC driver class 3 Class driverFromCCL = null; 4 if (driverClassName != null) { 5 try { 6 try { 7 if (driverClassLoader == null) { 8 Class.forName(driverClassName); 9 } else { 10 Class.forName(driverClassName, true, driverClassLoader); 11 } 12 } catch (ClassNotFoundException cnfe) { 13 driverFromCCL = Thread.currentThread( 14 ).getContextClassLoader().loadClass( 15 driverClassName); 16 } 17 } catch (Throwable t) { 18 String message = "Cannot load JDBC driver class ‘" + 19 driverClassName + "‘"; 20 logWriter.println(message); 21 t.printStackTrace(logWriter); 22 throw new SQLNestedException(message, t); 23 } 24 } 25 26 // Create a JDBC driver instance 27 Driver driver = null; 28 try { 29 if (driverFromCCL == null) { 30 driver = DriverManager.getDriver(url); 31 } else { 32 // Usage of DriverManager is not possible, as it does not 33 // respect the ContextClassLoader 34 driver = (Driver) driverFromCCL.newInstance(); 35 if (!driver.acceptsURL(url)) { 36 throw new SQLException("No suitable driver", "08001"); 37 } 38 } 39 } catch (Throwable t) { 40 String message = "Cannot create JDBC driver of class ‘" + 41 (driverClassName != null ? driverClassName : "") + 42 "‘ for connect URL ‘" + url + "‘"; 43 logWriter.println(message); 44 t.printStackTrace(logWriter); 45 throw new SQLNestedException(message, t); 46 } 47 48 // Can‘t test without a validationQuery 49 if (validationQuery == null) { 50 setTestOnBorrow(false); 51 setTestOnReturn(false); 52 setTestWhileIdle(false); 53 } 54 55 // Set up the driver connection factory we will use 56 String user = username; 57 if (user != null) { 58 connectionProperties.put("user", user); 59 } else { 60 log("DBCP DataSource configured without a ‘username‘"); 61 } 62 63 String pwd = password; 64 if (pwd != null) { 65 connectionProperties.put("password", pwd); 66 } else { 67 log("DBCP DataSource configured without a ‘password‘"); 68 } 69 70 ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driver, url, connectionProperties); 71 return driverConnectionFactory; 72 }
上面一連串代碼幹了什麽呢?其實就幹了兩件事:1、獲取數據庫驅動 2、使用驅動以及參數(url、username、password)構造一個工廠。一旦這個工廠構建完畢了,就可以來生成連接,而這個連接的生成其實是驅動加上配置來完成的.
3.3 創建連接池的過程
1 protected void createConnectionPool() { 2 // Create an object pool to contain our active connections 3 GenericObjectPool gop; 4 if ((abandonedConfig != null) && (abandonedConfig.getRemoveAbandoned())) { 5 gop = new AbandonedObjectPool(null,abandonedConfig); 6 } 7 else { 8 gop = new GenericObjectPool(); 9 } 10 gop.setMaxActive(maxActive); 11 gop.setMaxIdle(maxIdle); 12 gop.setMinIdle(minIdle); 13 gop.setMaxWait(maxWait); 14 gop.setTestOnBorrow(testOnBorrow); 15 gop.setTestOnReturn(testOnReturn); 16 gop.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); 17 gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun); 18 gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); 19 gop.setTestWhileIdle(testWhileIdle); 20 connectionPool = gop; 21 }
在創建連接池的時候,用到了common-pool裏的GenericObjectPool,對於JDBC連接的緩存以及管理其實是交給GenericObjectPool的,DBCP其實只是負責創建這樣一種pool然後使用它而已。
3.4 創建statement緩存池
一般來說,statement並不是重量級的對象,創建過程消耗的資源並不像JDBC連接那樣重,所以沒必要做緩存池化,這裏為了簡便起見,對此不做分析。
3.5 創建PoolableConnectionFactory
這一步是一個承上啟下的過程,承上在於利用上面兩部創建的連接工廠和連接池,構建PoolableConnectionFactory,啟下則在於為後面的向連接池裏添加連接做準備。
下面先上一張靜態的類關系圖:
1 xprotected void createPoolableConnectionFactory(ConnectionFactory driverConnectionFactory, 2 KeyedObjectPoolFactory statementPoolFactory, AbandonedConfig configuration) throws SQLException { 3 PoolableConnectionFactory connectionFactory = null; 4 try { 5 connectionFactory = 6 new PoolableConnectionFactory(driverConnectionFactory, 7 connectionPool, 8 statementPoolFactory, 9 validationQuery, 10 validationQueryTimeout, 11 connectionInitSqls, 12 defaultReadOnly, 13 defaultAutoCommit, 14 defaultTransactionIsolation, 15 defaultCatalog, 16 configuration); 17 validateConnectionFactory(connectionFactory); 18 } catch (RuntimeException e) { 19 throw e; 20 } catch (Exception e) { 21 throw new SQLNestedException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e); 22 } 23 }
可以看見,在創建PoolableConnectionFactory的時候,需要用到前面創建的driverConnectionFactory以及連接池connectionPool,那麽那個構造函數到底幹了先什麽呢?
1 public PoolableConnectionFactory( 2 ConnectionFactory connFactory, 3 ObjectPool pool, 4 KeyedObjectPoolFactory stmtPoolFactory, 5 String validationQuery, 6 int validationQueryTimeout, 7 Collection connectionInitSqls, 8 Boolean defaultReadOnly, 9 boolean defaultAutoCommit, 10 int defaultTransactionIsolation, 11 String defaultCatalog, 12 AbandonedConfig config) { 13 14 _connFactory = connFactory; 15 _pool = pool; 16 _config = config; 17 _pool.setFactory(this); 18 _stmtPoolFactory = stmtPoolFactory; 19 _validationQuery = validationQuery; 20 _validationQueryTimeout = validationQueryTimeout; 21 _connectionInitSqls = connectionInitSqls; 22 _defaultReadOnly = defaultReadOnly; 23 _defaultAutoCommit = defaultAutoCommit; 24 _defaultTransactionIsolation = defaultTransactionIsolation; 25 _defaultCatalog = defaultCatalog; 26 }
它在內部保存了真正的JDBC 連接的工廠以及連接池,然後,通過一句_pool.setFactory(this); 將它自己設置給了連接池。這行代碼十分重要,要理解這行代碼,首先需要明白common-pool中的GenericObjectPool添加內部元素的一般方法,沒錯,那就是必須要傳入一個工廠Factory。GenericObjectPool添加內部元素時會調用addObject()這個方法,內部其實是調用工廠的makeObejct()方法來創建元素,然後再加入到自己的池中。_pool.setFactory(this)這句代碼其實起到了啟下的作用,沒有它,後面的為連接池添加連接也就不可能完成。
當創建完工廠後,會有個validateConnectionFactory(connectionFactory);這個方法的作用僅僅是用來驗證數據庫連接可使用,看代碼:
1 protected static void validateConnectionFactory(PoolableConnectionFactory connectionFactory) throws Exception { 2 Connection conn = null; 3 try { 4 conn = (Connection) connectionFactory.makeObject(); 5 connectionFactory.activateObject(conn); 6 connectionFactory.validateConnection(conn); 7 connectionFactory.passivateObject(conn); 8 } 9 finally { 10 connectionFactory.destroyObject(conn); 11 } 12 }
先是用makeObject方法來創建一個連接,然後做相關驗證(就是用一些初始化sql來試著執行一下,看看能不能連接到數據庫),然後銷毀連接,這裏並沒有向連接池添加連接,真正的添加連接在後面,不過,我們可以先通過下面一張時序圖來看看makeObject方法到底做了什麽。
下面是一張整體流程的時序圖:
從圖中可以看出,makeObject方法的大致流程:從driverConnectionFactory那裏拿到底層連接,初始化驗證,然後創建PoolableConnection,在創建這個PoolableConnection的時候,將PoolableConnection與連接池關聯了起來,真正做到了連接池和連接之間的一對多的關系,這也為改變PoolableConnection的close方法提供了方便。
下面是makeObject方法的源代碼:
1 public Object makeObject() throws Exception { 2 Connection conn = _connFactory.createConnection(); 3 if (conn == null) { 4 throw new IllegalStateException("Connection factory returned null from createConnection"); 5 } 6 initializeConnection(conn); //初始化,這個過程可有可無 7 if(null != _stmtPoolFactory) { 8 KeyedObjectPool stmtpool = _stmtPoolFactory.createPool(); 9 conn = new PoolingConnection(conn,stmtpool); 10 stmtpool.setFactory((PoolingConnection)conn); 11 } 12 //這裏是關鍵 13 return new PoolableConnection(conn,_pool,_config); 14 }
其中PoolableConnection的構造函數如下:
public PoolableConnection(Connection conn, ObjectPool pool, AbandonedConfig config) { super(conn, config); _pool = pool; }
內部關聯了一個連接池,這個連接池的作用體現在PoolableConnection的close方法中:
1 public synchronized void close() throws SQLException { 2 if (_closed) { 3 // already closed 4 return; 5 } 6 7 boolean isUnderlyingConectionClosed; 8 try { 9 isUnderlyingConectionClosed = _conn.isClosed(); 10 } catch (SQLException e) { 11 try { 12 _pool.invalidateObject(this); // XXX should be guarded to happen at most once 13 } catch(IllegalStateException ise) { 14 // pool is closed, so close the connection 15 passivate(); 16 getInnermostDelegate().close(); 17 } catch (Exception ie) { 18 // DO NOTHING the original exception will be rethrown 19 } 20 throw (SQLException) new SQLException("Cannot close connection (isClosed check failed)").initCause(e); 21 } 22 23 if (!isUnderlyingConectionClosed) { 24 // Normal close: underlying connection is still open, so we 25 // simply need to return this proxy to the pool 26 try { 27 _pool.returnObject(this); // XXX should be guarded to happen at most once 28 } catch(IllegalStateException e) { 29 // pool is closed, so close the connection 30 passivate(); 31 getInnermostDelegate().close(); 32 } catch(SQLException e) { 33 throw e; 34 } catch(RuntimeException e) { 35 throw e; 36 } catch(Exception e) { 37 throw (SQLException) new SQLException("Cannot close connection (return to pool failed)").initCause(e); 38 } 39 } else { 40 // Abnormal close: underlying connection closed unexpectedly, so we 41 // must destroy this proxy 42 try { 43 _pool.invalidateObject(this); // XXX should be guarded to happen at most once 44 } catch(IllegalStateException e) { 45 // pool is closed, so close the connection 46 passivate(); 47 getInnermostDelegate().close(); 48 } catch (Exception ie) { 49 // DO NOTHING, "Already closed" exception thrown below 50 } 51 throw new SQLException("Already closed."); 52 } 53 }
一行_pool.returnObject(this)表明並非真的關閉了,而是返還給了連接池。
到這裏, PoolableConnectionFactory創建好了,它使用driverConnectionFactory來創建底層連接,通過makeObject來創建PoolableConnection,這個PoolableConnection通過與connectionPool關聯來達到改變close方法的作用,當PoolableConnectionFactory創建好的時候,它自己已經作為一個工廠類被設置到了connectionPool,後面connectionPool會使用這個工廠來生產PoolableConnection,而生成的所有的PoolableConnection都與connectionPool關聯起來了,可以從connectionPool取出,也可以還給connectionPool。接下來,讓我們來看一看到底怎麽去初始化connectionPool。
3.6 創建數據源並初始化連接池
createDataSourceInstance(); try { for (int i = 0 ; i < initialSize ; i++) { connectionPool.addObject(); } } catch (Exception e) { throw new SQLNestedException("Error preloading the connection pool", e); }
我們先看 createDataSourceInstance();
protected void createDataSourceInstance() throws SQLException { PoolingDataSource pds = new PoolingDataSource(connectionPool); pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); pds.setLogWriter(logWriter); dataSource = pds; }
其實就是創建一個PoolingDataSource,作為底層真正的數據源,這個PoolingDataSource比較簡單,這裏不做詳細介紹
接下來是一個for循環,通過調用connectionPool.addObject();來為連接池添加數據庫連接,下面是一張時序圖:
可以看出,在3.5中創建的PoolableConnectionFactory在這裏起作用了,addObject依賴的正是makeObject,而makeObject在上面也介紹過了。
到此為止,數據源創建好了,連接池裏也有了可以使用的連接,而且每個連接和連接池都做了關聯,改變了close的行為。這個時候BasicDataSource正是可以工作了,調用getConnection的時候,實際是調用底層數據源的getConnection,而底層數據源其實就是從連接池中獲取的連接。
四.總結
整個數據源最核心的其實就三個東西:一個是連接池,在這裏體現為common-pool中的GenericObjectPool,它負責緩存和管理連接,所有的配置策略都是由它管理。第二個是連接,這裏的連接就是PoolableConnection,當然它是對底層連接進行了封裝。第三個則是連接池和連接的關系,在此表現為一對多的互相引用。對數據源的構建則是對連接池,連接以及連接池與連接的關系的構建,掌握了這些點,就基本能掌握數據源的構建。
[數據庫連接池] Java數據庫連接池--DBCP淺析.