[数据库连接池] Java数据库连接池

前言
对于数据库连接池, 想必大家都已经不再陌生, 这里仅仅设计 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 &lt; 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 的例子, 然后根据例子来分析:

 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,当然它是对底层连接进行了封装。第三个则是连接池和连接的关系,在此表现为一对多的互相引用。对数据源的构建则是对连接池,连接以及连接池与连接的关系的构建,掌握了这些点,就基本能掌握数据源的构建。