源码详解系列(五) ------ C3P0的使用和分析(包括JNDI)

简介

c3p0是用于建立和管理链接,利用“池”的方式复用链接减小资源开销,和其余数据源同样,也具备链接数控制、链接可靠性测试、链接泄露控制、缓存语句等功能。目前,hibernate自带的链接池就是c3p0java

本文将包含如下内容(由于篇幅较长,可根据须要选择阅读):mysql

  1. c3p0的使用方法(入门案例、JDNI使用)
  2. c3p0的配置参数详解
  3. c3p0主要源码分析

使用例子-入门

需求

使用C3P0链接池获取链接对象,对用户数据进行简单的增删改查(sql脚本项目中已提供)。git

工程环境

JDK:1.8.0_201github

maven:3.6.1web

IDE:eclipse 4.12spring

mysql-connector-java:8.0.15sql

mysql:5.7 .28数据库

C3P0:0.9.5.3apache

主要步骤

  1. 编写c3p0.properties,设置数据库链接参数和链接池基本参数等api

  2. new一个ComboPooledDataSource对象,它会自动加载c3p0.properties

  3. 经过ComboPooledDataSource对象得到Connection对象

  4. 使用Connection对象对用户表进行增删改查

建立项目

项目类型Maven Project,打包方式war(其实jar也能够,之因此使用war是为了测试JNDI)。

引入依赖

这里引入日志包,主要为了看看链接池的建立过程,不引入不会有影响的。

<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- c3p0 -->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.3</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <!-- log -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

编写c3p0.properties

c3p0支持使用.xml.properties等文件来配置参数。本文用的是c3p0.properties做为配置文件,相比.xml文件我以为会直观一些。

配置文件路径在resources目录下,由于是入门例子,这里仅给出数据库链接参数和链接池基本参数,后面源码会对全部配置参数进行详细说明。另外,数据库sql脚本也在该目录下。

注意:文件名必须是c3p0.properties,不然不会自动加载(若是是.xml,文件名为c3p0-config.xml)。

# c3p0只是会将该驱动实例注册到DriverManager,不能保证最终用的是该实例,除非设置了forceUseNamedDriverClass
c3p0.driverClass=com.mysql.cj.jdbc.Driver
c3p0.forceUseNamedDriverClass=true
c3p0.jdbcUrl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
# 获取链接时使用的默认用户名
c3p0.user=root
# 获取链接时使用的默认用户密码
c3p0.password=root

####### Basic Pool Configuration ########
# 当没有空闲链接可用时,批量建立链接的个数
# 默认3
c3p0.acquireIncrement=3
# 初始化链接个数
# 默认3
c3p0.initialPoolSize=3
# 最大链接个数
# 默认15
c3p0.maxPoolSize=15
# 最小链接个数
# 默认3
c3p0.minPoolSize=3

获取链接池和获取链接

项目中编写了JDBCUtil来初始化链接池、获取链接、管理事务和释放资源等,具体参见项目源码。

路径:cn.zzs.c3p0

// 配置文件名为c3p0.properties,会自动加载。
        DataSource dataSource = new ComboPooledDataSource();
        // 获取链接
        Connection conn = dataSource.getConnection();

除了使用ComboPooledDataSourcec3p0还提供了静态工厂类DataSources,这个类能够建立未池化的数据源对象,也能够将未池化的数据源池化,固然,这种方式也会去自动加载配置文件。

// 获取未池化数据源对象
        DataSource ds_unpooled = DataSources.unpooledDataSource();
        // 将未池化数据源对象进行池化
        DataSource ds_pooled = DataSources.pooledDataSource(ds_unpooled);
        // 获取链接
        Connection connection = ds_pooled.getConnection();

编写测试类

这里以保存用户为例,路径在test目录下的cn.zzs.c3p0

@Test
    public void save() {
        // 建立sql
        String sql = "insert into demo_user values(null,?,?,?,?,?)";
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 得到链接
            connection = JDBCUtil.getConnection();
            // 开启事务设置非自动提交
            JDBCUtil.startTrasaction();
            // 得到Statement对象
            statement = connection.prepareStatement(sql);
            // 设置参数
            statement.setString(1, "zzf003");
            statement.setInt(2, 18);
            statement.setDate(3, new Date(System.currentTimeMillis()));
            statement.setDate(4, new Date(System.currentTimeMillis()));
            statement.setBoolean(5, false);
            // 执行
            statement.executeUpdate();
            // 提交事务
            JDBCUtil.commit();
        } catch(Exception e) {
            JDBCUtil.rollback();
            log.error("保存用户失败", e);
        } finally {
            // 释放资源
            JDBCUtil.release(connection, statement, null);
        }
    }

使用例子-经过JNDI获取数据源

需求

本文测试使用JNDI获取ComboPooledDataSourceJndiRefConnectionPoolDataSource对象,选择使用tomcat 9.0.21做容器。

若是以前没有接触过JNDI,并不会影响下面例子的理解,其实能够理解为像springbean配置和获取。

引入依赖

本文在入门例子的基础上增长如下依赖,由于是web项目,因此打包方式为war

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.2.1</version>
            <scope>provided</scope>
        </dependency>

编写context.xml

webapp文件下建立目录META-INF,并建立context.xml文件。这里面的每一个resource节点都是咱们配置的对象,相似于springbean节点。其中jdbc/pooledDS能够当作是这个beanid

注意,这里获取的数据源对象是单例的,若是但愿多例,能够设置singleton="false"

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource auth="Container"
              description="DB Connection"
              driverClass="com.mysql.cj.jdbc.Driver"
              maxPoolSize="4"
              minPoolSize="2"
              acquireIncrement="1"
              name="jdbc/pooledDS"
              user="root"
              password="root"
              factory="org.apache.naming.factory.BeanFactory"
              type="com.mchange.v2.c3p0.ComboPooledDataSource"
              jdbcUrl="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true" />
</Context>

编写web.xml

web-app节点下配置资源引用,每一个resource-env-ref指向了咱们配置好的对象。

<resource-ref>
        <res-ref-name>jdbc/pooledDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>

编写jsp

由于须要在web环境中使用,若是直接建类写个main方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp来测试吧。

c3p0提供了JndiRefConnectionPoolDataSource来支持JNDI(方式一),固然,咱们也能够采用常规方式获取JNDI的数据源(方式二)。由于我设置的数据源时单例的,因此,两种方式得到的是同一个数据源对象,只是方式一会将该对象再次包装。

<body>
    <%
    String jndiName = "java:comp/env/jdbc/pooledDS";
    // 方式一
    JndiRefConnectionPoolDataSource jndiDs = new JndiRefConnectionPoolDataSource();
    jndiDs.setJndiName(jndiName);
    System.err.println("方式一得到的数据源identityToken:" + jndiDs.getIdentityToken());
    Connection con2 = jndiDs.getPooledConnection().getConnection();
    // do something
    System.err.println("方式一得到的链接:" + con2);
    
    // 方式二
    InitialContext ic = new InitialContext();
    // 获取JNDI上的ComboPooledDataSource
    DataSource ds = (DataSource) ic.lookup(jndiName);
    System.err.println("方式二得到的数据源identityToken:" + ((ComboPooledDataSource)ds).getIdentityToken());
    Connection con = ds.getConnection();
    // do something
    System.err.println("方式二得到的链接:" + con);
    
    // 释放资源
    if (ds instanceof PooledDataSource){
      PooledDataSource pds = (PooledDataSource) ds;
      // 先看看当前链接池的状态
      System.err.println("num_connections: "      + pds.getNumConnectionsDefaultUser());
      System.err.println("num_busy_connections: " + pds.getNumBusyConnectionsDefaultUser());
      System.err.println("num_idle_connections: " + pds.getNumIdleConnectionsDefaultUser());
      pds.close();
    }else{
      System.err.println("Not a c3p0 PooledDataSource!");
    }
    %>
</body>

测试结果

打包项目在tomcat9上运行,访问 http://localhost:8080/C3P0-demo/testJNDI.jsp ,控制台打印以下内容:

方式一得到的数据源identityToken:1hge1hra7cdbnef1fooh9k|3c1e541
方式一得到的链接:com.mchange.v2.c3p0.impl.NewProxyConnection@2baa7911
方式二得到的数据源identityToken:1hge1hra7cdbnef1fooh9k|9c60446
方式二得到的链接:com.mchange.v2.c3p0.impl.NewProxyConnection@e712a7c
num_connections: 3
num_busy_connections: 2
num_idle_connections: 1

此时正在使用的链接对象有2个,即两种方式各持有1个,即印证了两种方式得到的是同一数据源。

配置文件详解

这部份内容是参考官网的,对应当前所用的0.9.5.3版本(官网地址)。

数据库链接参数

注意,这里在url后面拼接了多个参数用于避免乱码、时区报错问题。 补充下,若是不想加入时区的参数,能够在mysql命令窗口执行以下命令:set global time_zone='+8:00'

还有,若是是xml文件,记得将&改为&amp;

# c3p0只是会将该驱动实例注册到DriverManager,不能保证最终用的是该实例,除非设置了forceUseNamedDriverClass
c3p0.driverClass=com.mysql.cj.jdbc.Driver
c3p0.forceUseNamedDriverClass=true

c3p0.jdbcUrl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true

# 获取链接时使用的默认用户名
c3p0.user=root
# 获取链接时使用的默认用户密码
c3p0.password=root

链接池数据基本参数

这几个参数都比较经常使用,具体设置多少需根据项目调整。

####### Basic Pool Configuration ########
# 当没有空闲链接可用时,批量建立链接的个数
# 默认3
c3p0.acquireIncrement=3

# 初始化链接个数
# 默认3
c3p0.initialPoolSize=3

# 最大链接个数
# 默认15
c3p0.maxPoolSize=15

# 最小链接个数
# 默认3
c3p0.minPoolSize=3

链接存活参数

为了不链接泄露没法回收的问题,建议设置maxConnectionAgeunreturnedConnectionTimeout

# 最大空闲时间。超过将被释放
# 默认0,即不限制。单位秒
c3p0.maxIdleTime=0

# 最大存活时间。超过将被释放
# 默认0,即不限制。单位秒
c3p0.maxConnectionAge=1800

# 过量链接最大空闲时间。
# 默认0,即不限制。单位秒
c3p0.maxIdleTimeExcessConnections=0

# 检出链接未归还的最大时间。
# 默认0。即不限制。单位秒
c3p0.unreturnedConnectionTimeout=0

链接检查参数

针对链接失效和链接泄露的问题,建议开启空闲链接测试(异步),而不建议开启检出测试(从性能考虑)。另外,经过设置preferredTestQueryautomaticTestTable能够加快测试速度。

# c3p0建立的用于测试链接的空表的表名。若是设置了,preferredTestQuery将失效。
# 默认null
#c3p0.automaticTestTable=test_table

# 自定义测试链接的sql。若是没有设置,c3p0会去调用isValid方法进行校验(c3p0版本0.9.5及以上)
# null
c3p0.preferredTestQuery=select 1 from dual

# ConnectionTester实现类,用于定义如何测试链接
# com.mchange.v2.c3p0.impl.DefaultConnectionTester
c3p0.connectionTesterClassName=com.mchange.v2.c3p0.impl.DefaultConnectionTester

# 空闲链接测试周期
# 默认0,即不检验。单位秒
c3p0.idleConnectionTestPeriod=300

# 链接检入时测试(异步)。
# 默认false
c3p0.testConnectionOnCheckin=false

# 链接检出时测试。
# 默认false。建议不要设置为true。
c3p0.testConnectionOnCheckout=false

缓存语句

针对大部分数据库而言,开启缓存语句能够有效提升性能。

# 全部链接PreparedStatement的最大总数量。是JDBC定义的标准参数,c3p0建议使用自带的maxStatementsPerConnection
# 默认0。即不限制
c3p0.maxStatements=0

# 单个链接PreparedStatement的最大数量。
# 默认0。即不限制
c3p0.maxStatementsPerConnection=0

# 延后清理PreparedStatement的线程数。可设置为1。
# 默认0。即不限制
c3p0.statementCacheNumDeferredCloseThreads=0

失败重试参数

根据项目实际状况设置。

# 失败重试时间。
# 默认30。若是非正数,则将一直阻塞地去获取链接。单位毫秒。
c3p0.acquireRetryAttempts=30

# 失败重试周期。
# 默认1000。单位毫秒
c3p0.acquireRetryDelay=1000

# 当获取链接失败,是否标志数据源已损坏,再也不重试。
# 默认false。
c3p0.breakAfterAcquireFailure=false

事务相关参数

建议保留默认就行。

# 链接检入时是否自动提交事务。
# 默认false。但c3p0会自动回滚
c3p0.autoCommitOnClose=false

# 链接检入时是否强制c3p0不去提交或回滚事务,以及修改autoCommit
# 默认false。强烈建议不要设置为true。
c3p0.forceIgnoreUnresolvedTransactions=false

其余

# 链接检出时是否记录堆栈信息。用于在unreturnedConnectionTimeout超时时打印。
# 默认false。
c3p0.debugUnreturnedConnectionStackTraces=false

# 在获取、检出、检入和销毁时,对链接对象进行操做的类。
# 默认null。经过继承com.mchange.v2.c3p0.AbstractConnectionCustomizer来定义。
#c3p0.connectionCustomizerClassName

# 池耗尽时,获取链接最大等待时间。
# 默认0。即无限阻塞。单位毫秒
c3p0.checkoutTimeout=0

# JNDI数据源的加载URL
# 默认null
#c3p0.factoryClassLocation

# 是否同步方式检入链接
# 默认false
c3p0.forceSynchronousCheckins=false

# c3p0的helper线程最大任务时间
# 默认0。即不限制。单位秒
c3p0.maxAdministrativeTaskTime=0

# c3p0的helper线程数量
# 默认3
c3p0.numHelperThreads=3

# 类加载器来源
# 默认caller
#c3p0.contextClassLoaderSource

# 是否使用c3p0的AccessControlContext
c3p0.privilegeSpawnedThreads=false

源码分析

c3p0的源码真的很是难啃,没有注释也就算了,代码的格式也是很是奇葩。正由于这个缘由,我刚开始接触c3p0时,就没敢深究它的源码。如今硬着头皮再次来翻看它的源码,仍是花了我很多时间。

由于c3p0的部分方法调用过程比较复杂,因此,此次源码分析重点关注类与类的关系和一些重要功能的实现,不像以往还能够一步步地探索。

另外,c3p0大量使用了监听器和多线程,由于是JDK自带的功能,因此本文不会深究其原理。感兴趣的同窗,能够补充学习下,毕竟实际项目中也会使用到的。

建立数据源对象

咱们使用c3p0时,通常会以ComboPooledDataSource这个类为入口,那么就从这个类展开吧。首先,看下ComboPooledDataSourceUML图。

ComboPooledDataSource的UML图

下面重点说下几个类的做用:

类名 描述
DataSource 用于建立原生的Connection
ConnectionPoolDataSource 用于建立PooledConnection
PooledDataSource 用于支持对c3p0链接池中链接数量和状态等的监控
IdentityTokenized 用于支持注册功能。每一个DataSource实例都有一个identityToken,用于在C3P0Registry中注册
PoolBackedDataSourceBase 实现了IdentityTokenized接口,还持有PropertyChangeSupportVetoableChangeSupport对象,并提供了添加和移除监听器的方法
AbstractPoolBackedDataSource 实现了PooledDataSourceDataSource
AbstractComboPooledDataSource 提供了数据源参数配置的setter/getter方法
DriverManagerDataSource DataSource实现类,用于建立原生的Connection
WrapperConnectionPoolDataSource ConnectionPoolDataSource实现类,用于建立PooledConnection
C3P0PooledConnectionPoolManager 链接池管理器,很是重要。用于建立链接池,并持有链接池的Map(根据帐号密码匹配链接池)。

当咱们new一个ComboPooledDataSource对象时,主要作了几件事:

  1. 得到thisidentityToken,并注册到C3P0Registry
  2. 添加监听配置参数改变的Listenner
  3. 建立DriverManagerDataSourceWrapperConnectionPoolDataSource对象

固然,在此以前有某个静态代码块加载类配置文件,具体加载过程后续有空再作补充。

得到this的identityToken,并注册到C3P0Registry

c3p0里,每一个数据源都有一个惟一的身份标志identityToken,用于在C3P0Registry中注册。下面看看具体identityToken的获取,调用的是C3P0ImplUtilsallocateIdentityToken方法。

System.identityHashCode(o)是本地方法,即便咱们不重写hashCode,同一个对象得到的hashCode惟一且不变,甚至程序重启也是同样。这个方法仍是挺神奇的,感兴趣的同窗能够研究下具体原理。

public static String allocateIdentityToken(Object o) {
        if(o == null)
            return null;
        else {
            // 获取对象的identityHashCode,并转为16进制
            String shortIdToken = Integer.toString(System.identityHashCode(o), 16);
            String out;
            long count;
            StringBuffer sb = new StringBuffer(128);
            sb.append(VMID_PFX);
            // 判断是否拼接当前对象被查看过的次数
            if(ID_TOKEN_COUNTER != null && ((count = ID_TOKEN_COUNTER.encounter(shortIdToken)) > 0)) {
                sb.append(shortIdToken);
                sb.append('#');
                sb.append(count);
            } else
                sb.append(shortIdToken);
            out = sb.toString().intern();
            return out;
        }
    }

接下来,再来看下注册过程,调用的是C3P0Registryincorporate方法。

// 存放identityToken=PooledDataSource的键值对
    private static Map tokensToTokenized = new DoubleWeakHashMap();
    // 存放未关闭的PooledDataSource
    private static HashSet unclosedPooledDataSources = new HashSet();
    private static void incorporate(IdentityTokenized idt) {
        tokensToTokenized.put(idt.getIdentityToken(), idt);
        if(idt instanceof PooledDataSource) {
            unclosedPooledDataSources.add(idt);
            mc.attemptManagePooledDataSource((PooledDataSource)idt);
        }
    }

注册的过程仍是比较简单易懂,可是有个比较奇怪的地方,通常这种所谓的注册,都会提供某个方法,让咱们能够在程序的任何位置经过惟一标识去查找数据源对象。然而,即便咱们知道了某个数据源的identityToken,仍是获取不到对应的数据源,由于C3P0Registry并无提供相关的方法给咱们。

后来发现,咱们不能也不该该经过identityToken来查找数据源,而是应该经过dataSourceName来查找才对,这不,C3P0Registry就提供了这样的方法。因此,若是咱们想在程序的任何位置都能获取到数据源对象,应该再建立数据源时就设置好它的dataSourceName

public synchronized static PooledDataSource pooledDataSourceByName(String dataSourceName) {
        for(Iterator ii = unclosedPooledDataSources.iterator(); ii.hasNext();) {
            PooledDataSource pds = (PooledDataSource)ii.next();
            if(pds.getDataSourceName().equals(dataSourceName))
                return pds;
        }
        return null;
    }

添加监听配置参数改变的Listenner

接下来是到监听器的内容了。监听器的支持是jdk自带的,主要涉及到PropertyChangeSupportVetoableChangeSupport两个类,至于具体的实现机理不在本文讨论范围内,感兴趣的同窗能够补充学习下。

建立ComboPooledDataSource时,总共添加了三个监听器。

监听器 描述
PropertyChangeListener1 connectionPoolDataSource, numHelperThreads, identityToken改变后,重置C3P0PooledConnectionPoolManager
VetoableChangeListener connectionPoolDataSource改变前,校验新设置的对象是不是WrapperConnectionPoolDataSource对象,以及该对象中的DataSource是否DriverManagerDataSource对象,若是不是,会抛出异常
PropertyChangeListener2 connectionPoolDataSource改变后,修改this持有的DriverManagerDataSourceWrapperConnectionPoolDataSource对象

咱们能够看到,在PoolBackedDataSourceBase对象中,持有了PropertyChangeSupportVetoableChangeSupport对象,用于支持监听器的功能。

public class PoolBackedDataSourceBase extends IdentityTokenResolvable implements Referenceable, Serializable
{
    protected PropertyChangeSupport pcs = new PropertyChangeSupport( this );
    protected VetoableChangeSupport vcs = new VetoableChangeSupport( this );
}

经过以上过程,c3p0能够在参数改变前进行校验,在参数改变后重置某些对象。

建立DriverManagerDataSource

ComboPooledDataSource在实例化父类AbstractComboPooledDataSource时会去建立DriverManagerDataSourceWrapperConnectionPoolDataSource对象,这两个对象都是用于建立链接对象,后者依赖前者。

public AbstractComboPooledDataSource(boolean autoregister) {
        super(autoregister);
        // 建立DriverManagerDataSource和WrapperConnectionPoolDataSource对象
        dmds = new DriverManagerDataSource();
        wcpds = new WrapperConnectionPoolDataSource();
        // 将DriverManagerDataSource设置给WrapperConnectionPoolDataSource
        wcpds.setNestedDataSource(dmds);
        
        // 初始化属性connectionPoolDataSource
        this.setConnectionPoolDataSource(wcpds);
        // 注册监听器
        setUpPropertyEvents();
    }

前面已经讲过,DriverManagerDataSource能够用来获取原生的链接对象,因此它的功能有点相似于JDBCDriverManager

DriverManagerDataSource的UML图

建立DriverManagerDataSource实例主要作了三件事,以下:

public DriverManagerDataSource(boolean autoregister) {
        // 1. 得到this的identityToken,并注册到C3P0Registry  
        super(autoregister);
        // 2. 添加监听配置参数改变的Listenner(当driverClass属性更改时触发事件) 
        setUpPropertyListeners();
        // 3. 读取配置文件,初始化默认的user和password
        String user = C3P0Config.initializeStringPropertyVar("user", null);
        String password = C3P0Config.initializeStringPropertyVar("password", null);
        if(user != null)
            this.setUser(user);
        if(password != null)
            this.setPassword(password);
    }

建立WrapperConnectionPoolDataSource

下面再看看WrapperConnectionPoolDataSource,它能够用来获取PooledConnection

WrapperConnectionPoolDataSource的UML图

建立WrapperConnectionPoolDataSource,主要作了如下三件件事:

public WrapperConnectionPoolDataSource(boolean autoregister) {
        // 1. 得到this的identityToken,并注册到C3P0Registry
        super(autoregister);
        // 2. 添加监听配置参数改变的Listenner(当connectionTesterClassName属性更改时实例化ConnectionTester,当userOverridesAsString更改时从新解析字符串) 
        setUpPropertyListeners();
        // 3. 解析userOverridesAsString
        this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString(this.getUserOverridesAsString());
    }

以上基本将ComboPooledDataSource的内容讲完,下面介绍链接池的建立。

建立链接池对象

当咱们建立完数据源时,链接池并无建立,也就是说只有咱们调用getConnection时才会触发建立链接池。由于AbstractPoolBackedDataSource实现了DataSource,因此咱们能够在这个类看到getConnection的具体实现,以下。

public Connection getConnection() throws SQLException{
        PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
        return pc.getConnection();
    }

这个方法中getPoolManager()获得的就是咱们前面提到过的C3P0PooledConnectionPoolManager,而getPool()获得的是C3P0PooledConnectionPool

咱们先来看看这两个类(注意,图中的类展现的只是部分的属性和方法):

C3P0PooledConnectionPoolManager和C3P0PooledConnectionPool的UML图

下面介绍下这几个类:

类名 描述
C3P0PooledConnectionPoolManager 链接池管理器。主要用于获取/建立链接池,它持有DbAuth-C3P0PooledConnectionPool键值对的Map
C3P0PooledConnectionPool 链接池。主要用于检入和检出链接对象,实际调用的是其持有的BasicResourcePool对象
BasicResourcePool 资源池。主要用于检入和检出链接对象
PooledConnectionResourcePoolManager 资源管理器。主要用于建立新的链接对象,以及检入、检出或空闲时进行链接测试

建立链接池的过程能够归纳为四个步骤:

  1. 建立C3P0PooledConnectionPoolManager对象,开启另外一个线程来初始化timertaskRunnerdeferredStatementDestroyerrpfactauthsToPools等属性

  2. 建立默认帐号密码对应的C3P0PooledConnectionPool对象,并建立PooledConnectionResourcePoolManager对象

  3. 建立BasicResourcePool对象,建立initialPoolSize对应的初始链接,开启检查链接是否过时、以及检查空闲链接有效性的定时任务

这里主要分析下第四步。

建立BasicResourcePool对象

在这个方法里除了初始化许多属性以外,还会去建立initialPoolSize对应的初始链接,开启检查链接是否过时、以及检查空闲链接有效性的定时任务。

public BasicResourcePool(Manager mgr, int start, int min, int max, int inc, int num_acq_attempts, int acq_attempt_delay, long check_idle_resources_delay, long max_resource_age, long max_idle_time, long excess_max_idle_time, long destroy_unreturned_resc_time, long expiration_enforcement_delay, boolean break_on_acquisition_failure, boolean debug_store_checkout_exceptions, boolean force_synchronous_checkins, AsynchronousRunner taskRunner, RunnableQueue asyncEventQueue,
            Timer cullAndIdleRefurbishTimer, BasicResourcePoolFactory factory) throws ResourcePoolException {
        // ·······
        this.taskRunner = taskRunner;
        this.asyncEventQueue = asyncEventQueue;
        this.cullAndIdleRefurbishTimer = cullAndIdleRefurbishTimer;
        this.factory = factory;
        // 开启监听器支持
        if (asyncEventQueue != null)
            this.rpes = new ResourcePoolEventSupport(this);
        else
            this.rpes = null;
        // 确保初始链接数量,这里会去调用recheckResizePool()方法,后面还会讲到的
        ensureStartResources();
        // 若是设置maxIdleTime、maxConnectionAge、maxIdleTimeExcessConnections和unreturnedConnectionTimeout,会开启定时任务检查链接是否过时
        if(mustEnforceExpiration()) {
            this.cullTask = new CullTask();
            cullAndIdleRefurbishTimer.schedule(cullTask, minExpirationTime(), this.expiration_enforcement_delay);
        }
        // 若是设置idleConnectionTestPeriod,会开启定时任务检查空闲链接有效性
        if(check_idle_resources_delay > 0) {
            this.idleRefurbishTask = new CheckIdleResourcesTask();
            cullAndIdleRefurbishTimer.schedule(idleRefurbishTask, check_idle_resources_delay, check_idle_resources_delay);
        }
        // ·······
    }

看过c3p0源码就会发现,c3p0的开发真的很是喜欢监听器和多线程,正是由于这样,才致使它的源码阅读起来会比较吃力。为了方便理解,这里再补充解释下BasicResourcePool的几个属性:

属性 描述
BasicResourcePoolFactory factory 资源池工厂。用于建立BasicResourcePool
AsynchronousRunner taskRunner 异步线程。用于执行资源池中链接的建立、销毁
RunnableQueue asyncEventQueue 异步队列。用于存放链接检出时向ResourcePoolEventSupport报告的事件
ResourcePoolEventSupport rpes 用于支持监听器
Timer cullAndIdleRefurbishTimer 定时任务线程。用于执行检查链接是否过时、以及检查空闲链接有效性的任务
TimerTask cullTask 执行检查链接是否过时的任务
TimerTask idleRefurbishTask 检查空闲链接有效性的任务
HashSet acquireWaiters 存放等待获取链接的客户端
HashSet otherWaiters 当客户端试图检出某个链接,而该链接恰好被检查空闲链接有效性的线程占用,此时客户端就会被加入otherWaiters
HashMap managed 存放当前池中全部的链接对象
LinkedList unused 存放当前池中全部的空闲链接对象
HashSet excluded 存放当前池中已失效但还没检出或使用的链接对象
Set idleCheckResources 存放当前检查空闲链接有效性的线程占用的链接对象

以上,基本讲完获取链接池的部分,接下来介绍链接的建立。

建立链接对象

我总结下获取链接的过程,为如下几步:

  1. BasicResourcePool的空闲链接中获取,若是没有,会尝试去建立新的链接,固然,建立的过程也是异步的

  2. 开启缓存语句支持

  3. 判断链接是否正在被空闲资源检测线程使用,若是是,从新获取链接

  4. 校验链接是否过时

  5. 检出测试

  6. 判断链接原来的Statement是否是已经清除完,若是没有,从新获取链接

  7. 设置监听器后将链接返回给客户端

下面仍是从头至尾分析该过程的源码吧。

C3P0PooledConnectionPool.checkoutPooledConnection()

如今回到AbstractPoolBackedDataSourcegetConnection方法,获取链接对象时会去调用C3P0PooledConnectionPoolcheckoutPooledConnection()

// 返回的是NewProxyConnection对象
    public Connection getConnection() throws SQLException{
        PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
        return pc.getConnection();
    }   
    // 返回的是NewPooledConnection对象
    public PooledConnection checkoutPooledConnection() throws SQLException {
        // 从链接池检出链接对象
        PooledConnection pc = (PooledConnection)this.checkoutAndMarkConnectionInUse();
        // 添加监听器,当链接close时会触发checkin事件
        pc.addConnectionEventListener(cl);
        return pc;
    }

以前我一直有个疑问,PooledConnection对象并不持有链接池对象,那么当客户端调用close()时,链接不就不能还给链接池了吗?看到这里总算明白了,c3p0使用的是监听器的方式,当客户端调用close()方法时会触发监听器把链接checkin到链接池中。

C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse()

经过这个方法能够看到,从链接池检出链接的过程不断循环,除非咱们设置了checkoutTimeout,超时会抛出异常,又或者检出过程抛出了其余异常。

另外,由于c3p0checkin链接时清除Statement采用的是异步方式,因此,当咱们尝试再次检出该链接,有可能Statement还没清除完,这个时候咱们不得不将链接还回去,再尝试从新获取链接。

private Object checkoutAndMarkConnectionInUse() throws TimeoutException, CannotAcquireResourceException, ResourcePoolException, InterruptedException {
        Object out = null;
        boolean success = false;
        // 注意,这里会自旋直到成功得到链接对象,除非抛出超时等异常
        while(!success) {
            try {
                // 从BasicResourcePool中检出链接对象
                out = rp.checkoutResource(checkoutTimeout);
                if(out instanceof AbstractC3P0PooledConnection) {
                    // 检查该链接下的Statement是否是已经清除完,若是没有,还得从新获取链接
                    AbstractC3P0PooledConnection acpc = (AbstractC3P0PooledConnection)out;
                    Connection physicalConnection = acpc.getPhysicalConnection();
                    success = tryMarkPhysicalConnectionInUse(physicalConnection);
                } else
                    success = true; // we don't pool statements from non-c3p0 PooledConnections
            } finally {
                try {
                    // 若是检出了链接对象,但出现异常或者链接下的Statement还没清除完,那么就须要从新检入链接
                    if(!success && out != null)
                        rp.checkinResource(out);
                } catch(Exception e) {
                    logger.log(MLevel.WARNING, "Failed to check in a Connection that was unusable due to pending Statement closes.", e);
                }
            }
        }
        return out;
    }

BasicResourcePool.checkoutResource(long)

下面这个方法会采用递归方式不断尝试检出链接,只有设置了checkoutTimeout,或者抛出其余异常,才能从该方法中出来。

若是咱们设置了testConnectionOnCheckout,则进行链接检出测试,若是不合格,就必须销毁这个链接对象,并尝试从新检出。

public Object checkoutResource(long timeout) throws TimeoutException, ResourcePoolException, InterruptedException {
        try {
            Object resc = prelimCheckoutResource(timeout);

            // 若是设置了testConnectionOnCheckout,会进行链接检出测试,会去调用PooledConnectionResourcePoolManager的refurbishResourceOnCheckout方法
            boolean refurb = attemptRefurbishResourceOnCheckout(resc);

            synchronized(this) {
                // 链接测试不经过
                if(!refurb) {
                    // 清除该链接对象
                    removeResource(resc);
                    // 确保链接池最小容量,会去调用recheckResizePool()方法,后面还会讲到的
                    ensureMinResources();
                    resc = null;
                } else {
                    // 在asyncEventQueue队列中加入当前链接检出时向ResourcePoolEventSupport报告的事件
                    asyncFireResourceCheckedOut(resc, managed.size(), unused.size(), excluded.size());
                    PunchCard card = (PunchCard)managed.get(resc);
                    // 该链接对象被删除了??
                    if(card == null) // the resource has been removed!
                    {
                        if(Debug.DEBUG && logger.isLoggable(MLevel.FINER))
                            logger.finer("Resource " + resc + " was removed from the pool while it was being checked out " + " or refurbished for checkout. Will try to find a replacement resource.");
                        resc = null;
                    } else {
                        card.checkout_time = System.currentTimeMillis();
                    }
                }
            }
            // 若是检出失败,还会继续检出,除非抛出超时等异常
            if(resc == null)
                return checkoutResource(timeout);
            else
                return resc;
        } catch(StackOverflowError e) {
            throw new NoGoodResourcesException("After checking so many resources we blew the stack, no resources tested acceptable for checkout. " + "See logger com.mchange.v2.resourcepool.BasicResourcePool output at FINER/DEBUG for information on individual failures.", e);
        }
    }

BasicResourcePool.prelimCheckoutResource(long)

这个方法也是采用递归的方式不断地尝试获取空闲链接,只有设置了checkoutTimeout,或者抛出其余异常,才能从该方法中出来。

若是咱们开启了空闲链接检测,当咱们获取到某个空闲链接时,若是它正在进行空闲链接检测,那么咱们不得不等待,并尝试从新获取。

还有,若是咱们设置了maxConnectionAge,还必须校验当前获取的链接是否是已通过期,过时的话也得从新获取。

private synchronized Object prelimCheckoutResource(long timeout) throws TimeoutException, ResourcePoolException, InterruptedException {
        try {
            // 检验当前链接池是否已经关闭或失效
            ensureNotBroken();
            
            int available = unused.size();
            // 若是当前没有空闲链接
            if(available == 0) {
                int msz = managed.size();
                // 若是当前链接数量小于maxPoolSize,则能够建立新链接
                if(msz < max) {
                    // 计算想要的目标链接数=池中总链接数+等待获取链接的客户端数量+当前客户端
                    int desired_target = msz + acquireWaiters.size() + 1;

                    if(logger.isLoggable(MLevel.FINER))
                        logger.log(MLevel.FINER, "acquire test -- pool size: " + msz + "; target_pool_size: " + target_pool_size + "; desired target? " + desired_target);
                    // 若是想要的目标链接数不小于原目标链接数,才会去尝试建立新链接
                    if(desired_target >= target_pool_size) {
                        // inc是咱们一开始设置的acquireIncrement
                        desired_target = Math.max(desired_target, target_pool_size + inc);
                        // 确保咱们的目标数量不大于maxPoolSize,不小于minPoolSize
                        target_pool_size = Math.max(Math.min(max, desired_target), min);
                        // 这里就会去调整池中的链接数量
                        _recheckResizePool();
                    }
                } else {
                    if(logger.isLoggable(MLevel.FINER))
                        logger.log(MLevel.FINER, "acquire test -- pool is already maxed out. [managed: " + msz + "; max: " + max + "]");
                }
                // 等待可用链接,若是设置checkoutTimeout可能会抛出超时异常
                awaitAvailable(timeout); // throws timeout exception
            }
            // 从空闲链接中获取
            Object resc = unused.get(0);

            // 若是获取到的链接正在被空闲资源检测线程使用
            if(idleCheckResources.contains(resc)) {
                if(Debug.DEBUG && logger.isLoggable(MLevel.FINER))
                    logger.log(MLevel.FINER, "Resource we want to check out is in idleCheck! (waiting until idle-check completes.) [" + this + "]");

                // 须要再次等待后从新获取链接对象
                Thread t = Thread.currentThread();
                try {
                    otherWaiters.add(t);
                    this.wait(timeout);
                    ensureNotBroken();
                } finally {
                    otherWaiters.remove(t);
                }
                return prelimCheckoutResource(timeout);
            // 若是当前链接过时,须要从池中删除,并尝试从新获取链接
            } else if(shouldExpire(resc)) {
                if(Debug.DEBUG && logger.isLoggable(MLevel.FINER))
                    logger.log(MLevel.FINER, "Resource we want to check out has expired already. Trying again.");

                removeResource(resc);
                ensureMinResources();
                return prelimCheckoutResource(timeout);
            // 将链接对象从空闲队列中移出
            } else {
                unused.remove(0);
                return resc;
            }
        } catch(ResourceClosedException e) // one of our async threads died
            // ·······
        }
    }

BasicResourcePool._recheckResizePool()

从上个方法可知,当前没有空闲链接可用,且链接池中的链接还未达到maxPoolSize时,就能够尝试建立新的链接。在这个方法中,会计算须要增长的链接数。

private void _recheckResizePool() {
        assert Thread.holdsLock(this);
        
        if(!broken) {
            int msz = managed.size();

            int shrink_count;
            int expand_count;
            // 从池中清除指定数量的链接
            if((shrink_count = msz - pending_removes - target_pool_size) > 0)
                shrinkPool(shrink_count);
            // 从池中增长指定数量的链接
            else if((expand_count = target_pool_size - (msz + pending_acquires)) > 0)
                expandPool(expand_count);
        }
    }

BasicResourcePool.expandPool(int)

在这个方法中,会采用异步的方式来建立新的链接对象。c3p0挺奇怪的,动不动就异步?

private void expandPool(int count) {
        assert Thread.holdsLock(this);

        // 这里是采用异步方式获取链接对象的,具体有两个不一样人物类型,我暂时不知道区别
        if(USE_SCATTERED_ACQUIRE_TASK) {
            for(int i = 0; i < count; ++i)
                taskRunner.postRunnable(new ScatteredAcquireTask());
        } else {
            for(int i = 0; i < count; ++i)
                taskRunner.postRunnable(new AcquireTask());
        }
    }

ScatteredAcquireTaskAcquireTask都是BasicResourcePool的内部类,在它们的run方法中最终会去调用PooledConnectionResourcePoolManageracquireResource方法。

PooledConnectionResourcePoolManager.acquireResource()

在建立数据源对象时有提到WrapperConnectionPoolDataSource这个类,它能够用来建立PooledConnection。这个方法中就是调用WrapperConnectionPoolDataSource对象来获取PooledConnection对象(实现类NewPooledConnection)。

public Object acquireResource() throws Exception {
        PooledConnection out;
        // 通常咱们不回去设置connectionCustomizerClassName,因此直接看connectionCustomizer为空的状况
        if(connectionCustomizer == null) {
            // 会去调用WrapperConnectionPoolDataSource的getPooledConnection方法
            out = (auth.equals(C3P0ImplUtils.NULL_AUTH) ? cpds.getPooledConnection() : cpds.getPooledConnection(auth.getUser(), auth.getPassword()));
        } else {
            // ·····
        }
        
        // 若是开启了缓存语句
        if(scache != null) {
            if(c3p0PooledConnections)
                ((AbstractC3P0PooledConnection)out).initStatementCache(scache);
            else {
                logger.warning("StatementPooling not " + "implemented for external (non-c3p0) " + "ConnectionPoolDataSources.");
            }
        }
        // ······
        return out;
    }

WrapperConnectionPoolDataSource.getPooledConnection(String, String, ConnectionCustomizer, String)

这个方法会先获取物理链接,而后将物理链接包装成NewPooledConnection

protected PooledConnection getPooledConnection(String user, String password, ConnectionCustomizer cc, String pdsIdt) throws SQLException {
        // 这里得到的就是咱们前面提到的DriverManagerDataSource
        DataSource nds = getNestedDataSource();
        Connection conn = null;
        // 使用DriverManagerDataSource得到原生的Connection
        conn = nds.getConnection(user, password);
        // 通常咱们不会去设置usesTraditionalReflectiveProxies,因此只看false的状况
        if(this.isUsesTraditionalReflectiveProxies(user)) {
            return new C3P0PooledConnection(conn, 
                    connectionTester, 
                    this.isAutoCommitOnClose(user), 
                    this.isForceIgnoreUnresolvedTransactions(user), 
                    cc, 
                    pdsIdt);
        } else {
            // NewPooledConnection就是原生链接的一个包装类而已,没什么特别的
            return new NewPooledConnection(conn, 
                    connectionTester, 
                    this.isAutoCommitOnClose(user), 
                    this.isForceIgnoreUnresolvedTransactions(user), 
                    this.getPreferredTestQuery(user), 
                    cc, 
                    pdsIdt);
        }
    }

以上,基本讲完获取链接对象的过程,c3p0的源码分析也基本完成,后续有空再作补充。

参考资料

c3p0 - JDBC3 Connection and Statement Pooling by Steve Waldman

本文为原创文章,转载请附上原文出处连接:https://github.com/ZhangZiSheng001/c3p0-demo

相关文章
相关标签/搜索