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

前言

在上一篇文章中,我们结合实际情况,详细的讲解了如何使用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 源码解析,有兴趣的可以去看看,在这我就不赘述了。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 前言 Apache-Commons-DBCP是数据库连接池中一款优秀的产品,熟悉dbcp同学都知道,dbcp底层“...
    许da广阅读 2,572评论 1 6
  • 本文包括传统JDBC的缺点连接池原理自定义连接池开源数据库连接池DBCP连接池C3P0连接池Tomcat内置连接池...
    廖少少阅读 16,723评论 0 37
  • JDBC概述 在Java中,数据库存取技术可分为如下几类:JDBC直接访问数据库、JDO技术、第三方O/R工具,如...
    usopp阅读 3,526评论 3 75
  • 排骨要增肥__阅读 235评论 0 0
  • 我叫轻轻,今年二十一岁,生活在西安。西安是陕西的省会城市,经济挺发达的,对我来讲,它和上海、北京一样好。但是西安不...
    _喝醉了的风阅读 265评论 0 0