簡介
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
圖。
下面重點說下幾個類的作用:
類名 | 描述 |
---|---|
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
時,總共添加了三個監聽器。
監聽器 | 描述 |
---|---|
PropertyChangeListener 1 |
當connectionPoolDataSource , numHelperThreads , identityToken 改變后,重置C3P0PooledConnectionPoolManager |
VetoableChangeListener |
當connectionPoolDataSource 改變前,校驗新設置的對象是否是WrapperConnectionPoolDataSource 對象,以及該對象中的DataSource 是否DriverManagerDataSource 對象,如果不是,會拋出異常 |
PropertyChangeListener 2 |
當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
實例主要做了三件事,如下:
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
,主要做了以下三件件事:
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 |
連接池管理器。主要用於獲取/創建連接池,它持有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 Waldman
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12080533.html