推薦閱讀:
簡介
c3p0是用於創建和管理連接,利用“池”的方式複用連接減少資源開銷,和其他數據源一樣,也具有連接數控制、連接可靠性測試、連接泄露控制、緩存語句等功能。目前,hibernate自帶的連接池就是c3p0。
本文將包含以下內容(因爲篇幅較長,可根據需要選擇閱讀):
c3p0的使用方法(入門案例、JDNI使用)
c3p0的配置參數詳解
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
主要步驟
編寫c3p0.properties,設置數據庫連接參數和連接池基本參數等
new一個ComboPooledDataSource對象,它會自動加載c3p0.properties
通過ComboPooledDataSource對象獲得Connection對象
使用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&characterEncoding=utf8&serverTimezone=GMT%2B8&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對象時,主要做了幾件事:
獲得this的identityToken,並註冊到C3P0Registry
添加監聽配置參數改變的Listenner
創建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 | 資源管理器。主要用於創建新的連接對象,以及檢入、檢出或空閒時進行連接測試 |
創建連接池的過程可以概括爲四個步驟:
創建C3P0PooledConnectionPoolManager對象,開啓另一個線程來初始化timer、taskRunner、deferredStatementDestroyer、rpfact和authsToPools等屬性
創建默認賬號密碼對應的C3P0PooledConnectionPool對象,並創建PooledConnectionResourcePoolManager對象
創建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 | 存放當前檢查空閒連接有效性的線程佔用的連接對象 |
以上,基本講完獲取連接池的部分,接下來介紹連接的創建。
創建連接對象
我總結下獲取連接的過程,爲以下幾步:
從BasicResourcePool的空閒連接中獲取,如果沒有,會嘗試去創建新的連接,當然,創建的過程也是異步的
開啓緩存語句支持
判斷連接是否正在被空閒資源檢測線程使用,如果是,重新獲取連接
校驗連接是否過期
檢出測試
判斷連接原來的Statement是不是已經清除完,如果沒有,重新獲取連接
設置監聽器後將連接返回給客戶端
下面還是從頭到尾分析該過程的源碼吧。
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