源碼詳解系列(五) ------ 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

相關文章
相關標籤/搜索