前言
對於數據庫鏈接池, 想必你們都已經再也不陌生, 這裏僅僅設計Java中的兩個經常使用數據庫鏈接池: DBCP和C3P0(後續會更新).
一. 爲什麼要使用數據庫鏈接池
假設網站一天有很大的訪問量,數據庫服務器就須要爲每次鏈接建立一次數據庫鏈接,極大的浪費數據庫的資源,而且極易形成數據庫服務器內存溢出、拓機。
數據庫鏈接是一種關鍵的有限的昂貴的資源,這一點在多用戶的網頁應用程序中體現的尤其突出.對數據庫鏈接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響到程序的性能指標.數據庫鏈接池正式針對這個問題提出來的.數據庫鏈接池負責分配,管理和釋放數據庫鏈接,它容許應用程序重複使用一個現有的數據庫鏈接,而不是從新創建一個。
java
數據庫鏈接池在初始化時將建立必定數量的數據庫鏈接放到鏈接池中, 這些數據庫鏈接的數量是由最小數據庫鏈接數來設定的.不管這些數據庫鏈接是否被使用,鏈接池都將一直保證至少擁有這麼多的鏈接數量.鏈接池的最大數據庫鏈接數量限定了這個鏈接池能佔有的最大鏈接數,當應用程序向鏈接池請求的鏈接數超過最大鏈接數量時,這些請求將被加入到等待隊列中.mysql
數據庫鏈接池的最小鏈接數和最大鏈接數的設置要考慮到如下幾個因素:sql
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 的例子, 而後根據例子來分析:緩存
1 #鏈接設置 2 driverClassName=com.mysql.jdbc.Driver 3 url=jdbc:mysql://localhost:3306/day14 4 username=root 5 password=abc 6 7 #<!-- 初始化鏈接 --> 8 initialSize=10 9 10 #最大鏈接數量 11 maxActive=50 12 13 #<!-- 最大空閒鏈接 --> 14 maxIdle=20 15 16 #<!-- 最小空閒鏈接 --> 17 minIdle=5 18 19 #<!-- 超時等待時間以毫秒爲單位 60000毫秒/1000等於60秒 --> 20 maxWait=60000 21 22 23 #JDBC驅動創建鏈接時附帶的鏈接屬性屬性的格式必須爲這樣:[屬性名=property;] 24 #注意:"user" 與 "password" 兩個屬性會被明確地傳遞,所以這裏不須要包含他們。 25 connectionProperties=useUnicode=true;characterEncoding=utf8 26 27 #指定由鏈接池所建立的鏈接的自動提交(auto-commit)狀態。 28 defaultAutoCommit=true 29 30 #driver default 指定由鏈接池所建立的鏈接的只讀(read-only)狀態。 31 #若是沒有設置該值,則「setReadOnly」方法將不被調用。(某些驅動並不支持只讀模式,如:Informix) 32 defaultReadOnly= 33 34 #driver default 指定由鏈接池所建立的鏈接的事務級別(TransactionIsolation)。 35 #可用值爲下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE 36 defaultTransactionIsolation=REPEATABLE_READ
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)app
public Connection getConnection() throws SQLException { return createDataSource().getConnection(); }
3.1 接下來看createDataSoruce() 方法:eclipse
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步,逐步構造出一個數據源,下面是詳細的步驟:ide
一、檢查數據源是否關閉或者是否建立完成,若是關閉了就拋異常,若是已經建立完成就直接返回。函數
二、調用createConnectionFactory()建立JDBC鏈接工廠driverConnectionFactory,這個工廠使用數據庫驅動來建立最底層的JDBC鏈接
三、調用createConnectionPool()建立數據源使用的鏈接池,鏈接池顧名思義就是緩存JDBC鏈接的地方。
四、若是須要就設置statement的緩存池,這個通常不須要設置
五、調用createPoolableConnectionFactory建立PoolableConnection的工廠,這個工廠使用上述driverConnectionFactory來建立底層JDBC鏈接,而後包裝出一個PoolableConnection,這個PoolableConnection與鏈接池設置了一對多的關係,也就是說,鏈接池中存在多個PoolableConnection,每一個PoolableConnection都關聯同一個鏈接池,這樣的好處是便於該表PoolableConnection的close方法的行爲,具體會在後面詳細分析。
六、調用createDataSourceInstance()建立內部數據源
七、爲鏈接池中添加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 }
上面一連串代碼幹了什麼呢?其實就幹了兩件事:一、獲取數據庫驅動 二、使用驅動以及參數(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,固然它是對底層鏈接進行了封裝。第三個則是鏈接池和鏈接的關係,在此表現爲一對多的互相引用。對數據源的構建則是對鏈接池,鏈接以及鏈接池與鏈接的關係的構建,掌握了這些點,就基本能掌握數據源的構建。