c3p0
是用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘數據源同樣,也具備鏈接數控制、鏈接可靠性測試、鏈接泄露控制、緩存語句等功能。目前,hibernate
自帶的鏈接池就是c3p0
。java
本文將包含如下內容(由於篇幅較長,可根據須要選擇閱讀):mysql
c3p0
的使用方法(入門案例、JDNI
使用)c3p0
的配置參數詳解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
編寫c3p0.properties
,設置數據庫鏈接參數和鏈接池基本參數等api
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
支持使用.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() { // 建立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
獲取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>
在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-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>
由於須要在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
針對大部分數據庫而言,開啓緩存語句能夠有效提升性能。
# 全部鏈接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
對象固然,在此以前有某個靜態代碼塊加載類配置文件,具體加載過程後續有空再作補充。
在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; }
接下來是到監聽器的內容了。監聽器的支持是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
能夠在參數改變前進行校驗,在參數改變後重置某些對象。
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
,它能夠用來獲取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
對應的初始鏈接,開啓檢查鏈接是否過時、以及檢查空閒鏈接有效性的定時任務
這裏主要分析下第四步。
在這個方法裏除了初始化許多屬性以外,還會去建立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是否是已經清除完,若是沒有,從新獲取鏈接
設置監聽器後將鏈接返回給客戶端
下面仍是從頭至尾分析該過程的源碼吧。
如今回到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
到鏈接池中。
經過這個方法能夠看到,從鏈接池檢出鏈接的過程不斷循環,除非咱們設置了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; }
下面這個方法會採用遞歸方式不斷嘗試檢出鏈接,只有設置了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); } }
這個方法也是採用遞歸的方式不斷地嘗試獲取空閒鏈接,只有設置了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 // ······· } }
從上個方法可知,當前沒有空閒鏈接可用,且鏈接池中的鏈接還未達到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); } }
在這個方法中,會採用異步的方式來建立新的鏈接對象。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
方法。
在建立數據源對象時有提到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; }
這個方法會先獲取物理鏈接,而後將物理鏈接包裝成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