前言
对于数据库链接池, 想必你们都已经再也不陌生, 这里仅仅设计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,固然它是对底层链接进行了封装。第三个则是链接池和链接的关系,在此表现为一对多的互相引用。对数据源的构建则是对链接池,链接以及链接池与链接的关系的构建,掌握了这些点,就基本能掌握数据源的构建。