前言
在上一篇文章中,我们结合实际情况,详细的讲解了如何使用dbcp,但是,想要深入了解这款产品,学会使用仅仅是第一步。本篇我们就更进一步,在学会使用的基础上,结合源码,更加深入的了解dbcp的工作原理和设计思路。
通用的核心功能与设计思路
在仔细分析dbcp的工作原理之前,我想先花点时间说一下数据库连接池的通用核心功能和设计思路。数据库连接池和RDBMS(关系型数据库管理系统)类似,核心功能与设计思路这两块东西基本都是通用的,同个产品的不同版本之间的差异,以及不同产品之间的差异,主要在构架设计和实现质量上。把这两项搞清楚再去进行源码分析,会有一种了然于胸的感觉,对于理解和学习作者们的思路和架构技巧,有非常大的帮助。
那一个数据库连接池的核心功能是什么呢?在回答这个问题之前让我们先回想一下,假如我们不使用dbcp,要完成一次数据库的操作需要哪些步骤呢?有熟悉JDBC编程的同学可能很快就能写出下面这些代码:
public static void main(String[] args) {
String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
String url = "jdbc:sqlserver://127.0.0.1:1433;DatabaseName=******";
String username = "***";
String password = "***";
String sql = "select * from table where ***";
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
Class.forName(driverName);
// 使用DriverManager属于一种非常古老的写法,但好处是足够通用
// DBCP提供了PoolingDriver类,用来支持此种写法
// conn = DriverManager.getConnection(url, username, password);
// Java 1.4 之后,可以选择使用由个数据库驱动提供的database接口的实现,来获取connection
OracleConnectionPoolDataSource dataSource = new OracleConnectionPoolDataSource();
dataSource.setURL(url);
dataSource.setUser(username);
dataSource.setPassword(password)
Connection conn = dataSource.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getInt("id"));
}
rs.close();
stmt.close();
conn.close();
}
如果我们拿之前使用了dbcp的代码来和上面那段代码对比的话,会发现一个数据库连接池应至少具备以下功能:
- 连接池要保存一组java.sql.Connection对象;
- 连接池应中保存的connection对象个数应该在可控的范围之内;
- 连接池要便于使用,能够灵活替换。
那究竟要如何实现这些功能呢?让我们对照着上面的功能来看。
- 要保存一组数据对象,最方便的方法,就是使用原生java.util.Collection及其子类,但是受限于他们的应用场景和性能表现,当在高并发等严苛的系统的环境中时,更多的设计者们会选择自己设计实现Collection。
- 要实现“可控”,连接池就应该首先具备创建和销毁池中对象的能力,然后提供一组配置项给用户(最多能够容纳的个数,最小容纳的个数等),让用户按照自己的意愿来确定连接池的边界,最后连接池按照配置,动态的维护池中的数据。
- 一个数据库连接池对外提供服务的类,应该是javax.sql.DataSource的实现,这样就可以方便的用户替换不同的数据源;同时它内部维护的connection对象,应该重写原生物理connection对的close()方法,保证connection资源的回收动作,完全交由连接池操作。
dbcp的设计实现
在介绍了数据库连接池的基本功能和设计思路之后,我们就紧接着来看看,dbcp是如何实现上述功能的。
功能1、功能2
在我们阅读源码的过程中,我们发现,dbcp将功能1与功能2完全委托给Apache Commons Pool去实现,关于pool,我在另一篇文章中有详细的介绍,这里对照着上面说的功能点,再给大家说道说道。先说功能1,pool用来存储对象的容器为CursorableLinkedList,它实现了List接口,由Apache Commons Collections(version 3.1)提供,它最大的特点就是提供了一个ListIterator(集合迭代器)对象,允许实时修改其内部list的数据。对于个功能2,pool提供了GenericObjectPool类来实现对池中对象的控制,在前上一篇文章中,我们说道dbcp配置项中的有池的属性,以及高可用等属性,这些配置,实际上就是用来创建GenericObjectPool对象用的,这些配置就是GenericObjectPool的配置。另外,GenericObjectPool要求用户必须提供一个实现了org.apache.commons.pool.PoolableObjectFactory接口工厂类,看这个接口提供的方法,
// 创建一个对象
org.apache.commons.pool.PoolableObjectFactory.makeObject()
// 销毁一个对象
org.apache.commons.pool.PoolableObjectFactory.destroyObject(Object)
// 校验一个对象
org.apache.commons.pool.PoolableObjectFactory.validateObject(Object)
// 激活一个对象
org.apache.commons.pool.PoolableObjectFactory.activateObject(Object)
// 取消激活一个对象
org.apache.commons.pool.PoolableObjectFactory.passivateObject(Object)
我们就知道这个工厂类的作用了。
功能3
真正由dbcp自己实现的,其实只有功能3。那我们就先来看看,dbcp对外提供服务的BasicDataSource(org.apache.commons.dbcp.BasicDataSource)类究竟是如何设计实现的。按照内部对象的类型,我将整个类分为三块去分析。
内部属性
最开始我们使用dbcp的时候,就接触到这些属性,在本系列的第一篇文章中,我对这些属性做了分类并分别介绍了它们的用法,而且我们也知道,这些属性除了构造一个“物理连接”时用到的属性,都是用来创建GenericObjectPool对象的,所以这里就不重复的介绍了。-
接口方法实现:看完这个类的属性,就要看它接口方法的实现了。
- DataSource:getConnection(String, String):调用这个方法时直接抛UnsupportedOperationException,BasicDataSource不支持此类调用
- DataSource:getConnection():只有一行代码
createDataSource().getConnection();
createDataSource是内部方法,我们也自然而然,将目光对准了这些内部方法。
- 内部方法:dbcp主要由下面几个内部方法:
- createDataSource()
- createConnectionFactory()
- createConnectionPool()
- createDataSourceInstance()
- createPoolableConnectionFactory(ConnectionFactory, KeyedObjectPoolFactory, AbandonedConfig)
这几个方法的关系,如下图所示:
只看图可能比较抽象,我还是结合代码来为大家讲解,createDataSource()的源码如下
protected synchronized DataSource createDataSource()
throws SQLException {
// 首先判断容器(连接池)是否关闭,如果关闭的话直接抛异常
// closed是一个内部属性
if (closed) {
throw new SQLException("Data source is closed");
}
// 如果已经创建了dataSource,就直接返回,这个dataSource也是内部属性对象
if (dataSource != null) {
return (dataSource);
}
// 首先创建一个获得数据库物理连接的工厂类
ConnectionFactory driverConnectionFactory = createConnectionFactory();
// 创建连接池
// 方法内部的关键代码为:
// GenericObjectPool gop = new GenericObjectPool();
// connectionPool = gop;
// GenericObjectPool是apache.commons.pool中的组件
// 注意:此时这个connectionPool还无法创建数据对象
createConnectionPool();
// 根据配置,判断是否需要创建一个statement的pool工厂
// 最终这个工厂类将会委派给本地的connectionPool对象
// 如果poolPreparedStatements=true
// connectionPool创建一个connection时,会同时创建一个statement的池
GenericKeyedObjectPoolFactory statementPoolFactory = null;
if (isPoolPreparedStatements()) {
statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
-1, // unlimited maxActive (per key)
GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
0, // maxWait
1, // maxIdle (per key)
maxOpenPreparedStatements);
}
// 创建一个数据对象的工厂给connectionPool,让connectionPool可以创建数据库连接
createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig);
// 创建内部对象dataSource,由它来维护connectionPool对象
// 这个内部的dataSource对象为PoolingDataSource的实例,其内部对象_pool是指向connectionPool的引用
createDataSourceInstance();
// 根据创建的配置“蓄池”
try {
for (int i = 0 ; i < initialSize ; i++) {
// 使用数据对象工厂,创建物理数据库连接并交由connectionPool维护
connectionPool.addObject();
}
} catch (Exception e) {
throw new SQLNestedException("Error preloading the connection pool", e);
}
// 返回内部dataSource对象
return dataSource;
整个方法的核心是在createPoolableConnectionFactory(ConnectionFactory, KeyedObjectPoolFactory, AbandonedConfig),这个方法的主体就是调用构造方法,创建一个PoolableConnectionFactory对象,这个工厂对象,就是交给GenericObjectPool对象,管理和维护池中数据用的。
protected void createPoolableConnectionFactory(ConnectionFactory driverConnectionFactory,
KeyedObjectPoolFactory statementPoolFactory, AbandonedConfig configuration) throws SQLException {
PoolableConnectionFactory connectionFactory = null;
try {
connectionFactory =
new PoolableConnectionFactory(driverConnectionFactory,
connectionPool,
statementPoolFactory,
validationQuery,
validationQueryTimeout,
connectionInitSqls,
defaultReadOnly,
defaultAutoCommit,
defaultTransactionIsolation,
defaultCatalog,
configuration);
validateConnectionFactory(connectionFactory);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new SQLNestedException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e);
}
}
要注意这个PoolableConnectionFactory的构造方法
public PoolableConnectionFactory(
ConnectionFactory connFactory,
ObjectPool pool,
KeyedObjectPoolFactory stmtPoolFactory,
String validationQuery,
int validationQueryTimeout,
Collection connectionInitSqls,
Boolean defaultReadOnly,
boolean defaultAutoCommit,
int defaultTransactionIsolation,
String defaultCatalog,
AbandonedConfig config) {
// 创建数据库物理连接的工厂
_connFactory = connFactory;
// 指向connectionPool的引用
_pool = pool;
_config = config;
_pool.setFactory(this);
_stmtPoolFactory = stmtPoolFactory;
_validationQuery = validationQuery;
_validationQueryTimeout = validationQueryTimeout;
_connectionInitSqls = connectionInitSqls;
_defaultReadOnly = defaultReadOnly;
_defaultAutoCommit = defaultAutoCommit;
_defaultTransactionIsolation = defaultTransactionIsolation;
_defaultCatalog = defaultCatalog;
}
它将connectionPool作为它自己的内部对象,同时又将自己作为内部对象,保存到connectionPool里(调用 _pool.setFactory(this);),在整个BasicDataSource的生命周期中,connectionPool是一直存在的,这样就导致connectionPool和poolableConnectionFactory对象彼此嵌套依赖,为了印证我的想法,我特意断点跟踪了一下,发现实际情况也确实如此。
说实话我不是很认可这种设计,也可能是我功力不够,所以这里先留个引子,后续如果对这块的设计有更好的理解,再来更新。
至此,功能3还仅实现了一半,另一半功能由PoolableConnectionFactory实现。让我们来看看,由PoolableConnectionFactory创建出来的connection对象,相对于物理connection对象,有哪些不同。
public Object makeObject() throws Exception {
Connection conn = _connFactory.createConnection();
....
return new PoolableConnection(conn,_pool,_config);
}
可以看到,PoolableConnectionFactory创建出来的是一个PoolableConnection对象的实例,它同时保留这物理数据库连接对象的引用和连接池对象的引用。PoolableConnection的类图如下,它重写了close()方法
public synchronized void close() throws SQLException {
......
boolean isUnderlyingConectionClosed;
// 连接是否已经关闭
isUnderlyingConectionClosed = _conn.isClosed();
if (!isUnderlyingConectionClosed) {
// 将这个连接还给池
_pool.returnObject(this);
} else {
// 如果这个物理连接实际上已经关闭了的话,就不能在还给连接池了,而是应该销毁它
_pool.invalidateObject(this);
}
......
}
透过现象看本质
至此,我们已经基本理清楚dbcp对于其核心功能是如何实现的了,但这些“实现”还是有些太过流于表面,说白点,我们仅仅是知其然,但不知其所以然,其实根本就没有触及到一个数据库连接池的本质,那就是如何高效稳定的处理并发访问!dbcp在解决这个问题的时候,找了自己的好兄弟Apache Commons Pool,我之前有文章专门讲解过,传送门在这apache-commons-pool-1.5.4 源码解析,有兴趣的可以去看看,在这我就不赘述了。