美文网首页
全面掌握apache-commons-dbcp之二:核心功能的设

全面掌握apache-commons-dbcp之二:核心功能的设

作者: 许da广 | 来源:发表于2017-03-08 15:05 被阅读535次

    前言

    在上一篇文章中,我们结合实际情况,详细的讲解了如何使用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的代码来和上面那段代码对比的话,会发现一个数据库连接池应至少具备以下功能:

    1. 连接池要保存一组java.sql.Connection对象;
    2. 连接池应中保存的connection对象个数应该在可控的范围之内;
    3. 连接池要便于使用,能够灵活替换。

    那究竟要如何实现这些功能呢?让我们对照着上面的功能来看。

    • 要保存一组数据对象,最方便的方法,就是使用原生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)

    这几个方法的关系,如下图所示:


    dbcp内部方法的关系

    只看图可能比较抽象,我还是结合代码来为大家讲解,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对象彼此嵌套依赖,为了印证我的想法,我特意断点跟踪了一下,发现实际情况也确实如此。


    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 源码解析,有兴趣的可以去看看,在这我就不赘述了。

    参考

    相关文章

      网友评论

          本文标题:全面掌握apache-commons-dbcp之二:核心功能的设

          本文链接:https://www.haomeiwen.com/subject/ywmbgttx.html