咋回事!現在新人連數據庫連接池c3p0都沒有聽說過? 簡介 使用例子-入門 使用例子-通過JNDI獲取數據源 配置文件詳解 源碼分析 參考資料

推薦閱讀:

簡介

c3p0是用於創建和管理連接,利用“池”的方式複用連接減少資源開銷,和其他數據源一樣,也具有連接數控制、連接可靠性測試、連接泄露控制、緩存語句等功能。目前,hibernate自帶的連接池就是c3p0。

本文將包含以下內容(因爲篇幅較長,可根據需要選擇閱讀):

  1. c3p0的使用方法(入門案例、JDNI使用)

  2. c3p0的配置參數詳解

  3. c3p0主要源碼分析

使用例子-入門

需求

使用C3P0連接池獲取連接對象,對用戶數據進行簡單的增刪改查(sql腳本項目中已提供)。

工程環境

JDK:1.8.0_201

maven:3.6.1

IDE:eclipse 4.12

mysql-connector-java:8.0.15

mysql:5.7 .28

C3P0:0.9.5.3

主要步驟

  1. 編寫c3p0.properties,設置數據庫連接參數和連接池基本參數等

  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();

除了使用ComboPooledDataSource,c3p0還提供了靜態工廠類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() throws SQLException {
        // 創建sql
        String sql = "insert into demo_user values(null,?,?,?,?,?)";
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 獲得連接
            connection = JDBCUtil.getConnection();
            // 開啓事務設置非自動提交
            connection.setAutoCommit(false);
            // 獲得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();
            // 提交事務
            connection.commit();
        } finally {
            // 釋放資源
            JDBCUtil.release(connection, statement, null);
        }
    }

使用例子-通過JNDI獲取數據源

需求

本文測試使用JNDI獲取ComboPooledDataSource和JndiRefConnectionPoolDataSource對象,選擇使用tomcat 9.0.21作容器。

如果之前沒有接觸過JNDI,並不會影響下面例子的理解,其實可以理解爲像spring的bean配置和獲取。

引入依賴

本文在入門例子的基礎上增加以下依賴,因爲是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節點都是我們配置的對象,類似於spring的bean節點。其中jdbc/pooledDS可以看成是這個bean的id。

注意,這裏獲取的數據源對象是單例的,如果希望多例,可以設置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文件,記得將&改成&。

# 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

連接存活參數

爲了避免連接泄露無法回收的問題,建議設置maxConnectionAge和unreturnedConnectionTimeout。

# 最大空閒時間。超過將被釋放
# 默認0,即不限制。單位秒
c3p0.maxIdleTime=0

# 最大存活時間。超過將被釋放
# 默認0,即不限制。單位秒
c3p0.maxConnectionAge=1800

# 過量連接最大空閒時間。
# 默認0,即不限制。單位秒
c3p0.maxIdleTimeExcessConnections=0

# 檢出連接未歸還的最大時間。
# 默認0。即不限制。單位秒
c3p0.unreturnedConnectionTimeout=0

連接檢查參數

針對連接失效和連接泄露的問題,建議開啓空閒連接測試(異步),而不建議開啓檢出測試(從性能考慮)。另外,通過設置preferredTestQuery或automaticTestTable可以加快測試速度。

# 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

緩存語句

PSCache對支持遊標的數據庫性能提升巨大,比如說oracle。在mysql下建議關閉。

# 所有連接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這個類爲入口,那麼就從這個類展開吧。首先,看下ComboPooledDataSource的UML圖。

ComboPooledDataSource的UML圖

下面重點說下幾個類的作用:

類名 描述
DataSource 用於創建原生的Connection
ConnectionPoolDataSource 用於創建PooledConnection
PooledDataSource 用於支持對c3p0連接池中連接數量和狀態等的監控
IdentityTokenized 用於支持註冊功能。每個DataSource實例都有一個identityToken,用於在C3P0Registry中註冊
PoolBackedDataSourceBase 實現了IdentityTokenized接口,還持有PropertyChangeSupport和VetoableChangeSupport對象,並提供了添加和移除監聽器的方法
AbstractPoolBackedDataSource 實現了PooledDataSource和DataSource
AbstractComboPooledDataSource 提供了數據源參數配置的setter/getter方法
DriverManagerDataSource DataSource實現類,用於創建原生的Connection
WrapperConnectionPoolDataSource ConnectionPoolDataSource實現類,用於創建PooledConnection
C3P0PooledConnectionPoolManager 連接池管理器,非常重要。用於創建連接池,並持有連接池的Map(根據賬號密碼匹配連接池)。

當我們new一個ComboPooledDataSource對象時,主要做了幾件事:

  1. 獲得this的identityToken,並註冊到C3P0Registry

  2. 添加監聽配置參數改變的Listenner

  3. 創建DriverManagerDataSource和WrapperConnectionPoolDataSource對象

當然,在此之前有某個靜態代碼塊加載類配置文件,具體加載過程後續有空再做補充。

獲得this的identityToken,並註冊到C3P0Registry

在c3p0裏,每個數據源都有一個唯一的身份標誌identityToken,用於在C3P0Registry中註冊。下面看看具體identityToken的獲取,調用的是C3P0ImplUtils的allocateIdentityToken方法。

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;
        }
    }

接下來,再來看下注冊過程,調用的是C3P0Registry的incorporate方法。

    // 存放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自帶的,主要涉及到PropertyChangeSupport和VetoableChangeSupport兩個類,至於具體的實現機理不在本文討論範圍內,感興趣的同學可以補充學習下。

創建ComboPooledDataSource時,總共添加了三個監聽器。

監聽器 描述
PropertyChangeListener1 當connectionPoolDataSource, numHelperThreads, identityToken改變後,重置C3P0PooledConnectionPoolManager
VetoableChangeListener 當connectionPoolDataSource改變前,校驗新設置的對象是否是WrapperConnectionPoolDataSource對象,以及該對象中的DataSource是否DriverManagerDataSource對象,如果不是,會拋出異常
PropertyChangeListener2 當connectionPoolDataSource改變後,修改this持有的DriverManagerDataSource和WrapperConnectionPoolDataSource對象

我們可以看到,在PoolBackedDataSourceBase對象中,持有了PropertyChangeSupport和VetoableChangeSupport對象,用於支持監聽器的功能。

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

通過以上過程,c3p0可以在參數改變前進行校驗,在參數改變後重置某些對象。

創建DriverManagerDataSource

ComboPooledDataSource在實例化父類AbstractComboPooledDataSource時會去創建DriverManagerDataSource和WrapperConnectionPoolDataSource對象,這兩個對象都是用於創建連接對象,後者依賴前者。

    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可以用來獲取原生的連接對象,所以它的功能有點類似於JDBC的DriverManager。

DriverManagerDataSource的UML圖

創建DriverManagerDataSource實例主要做了三件事,如下:

    public DriverManagerDataSource(boolean autoregister) {

創建WrapperConnectionPoolDataSource

下面再看看WrapperConnectionPoolDataSource,它可以用來獲取PooledConnection。

WrapperConnectionPoolDataSource的UML圖

創建WrapperConnectionPoolDataSource,主要做了以下三件件事:

    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);
    }

以上基本將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對象,開啓另一個線程來初始化timer、taskRunner、deferredStatementDestroyer、rpfact和authsToPools等屬性

  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()

現在回到AbstractPoolBackedDataSource的getConnection方法,獲取連接對象時會去調用C3P0PooledConnectionPool的checkoutPooledConnection()。

    // 返回的是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,超時會拋出異常,又或者檢出過程拋出了其他異常。

另外,因爲c3p0在checkin連接時清除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());
        }
    }

ScatteredAcquireTask和AcquireTask都是BasicResourcePool的內部類,在它們的run方法中最終會去調用PooledConnectionResourcePoolManager的acquireResource方法。

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 Waldma

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章