DBCP
用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘鏈接池同樣,也具備鏈接數控制、鏈接有效性檢測、鏈接泄露控制、緩存語句等功能。目前,tomcat
自帶的鏈接池就是DBCP
,Spring開發組也推薦使用DBCP
,阿里的druid
也是參照DBCP
開發出來的。html
DBCP
除了咱們熟知的使用方式外,還支持經過JNDI
獲取數據源,並支持獲取JTA
或XA
事務中用於2PC
(兩階段提交)的鏈接對象,本文也將以例子說明。java
本文將包含如下內容(由於篇幅較長,可根據須要選擇閱讀):mysql
DBCP
的使用方法(入門案例說明);DBCP
的配置參數詳解;DBCP
主要源碼分析;DBCP
其餘特性的使用方法,如JNDI
和JTA
支持。使用DBCP
鏈接池獲取鏈接對象,對用戶數據進行簡單的增刪改查。git
JDK
:1.8.0_201github
maven
:3.6.1web
IDE
:eclipse 4.12spring
mysql-connector-java
:8.0.15sql
mysql
:5.7.28數據庫
DBCP
:2.6.0apache
編寫dbcp.properties
,設置數據庫鏈接參數和鏈接池基本參數等。
經過BasicDataSourceFactory
加載dbcp.properties
,並得到BasicDataDource
對象。
經過BasicDataDource
對象獲取Connection
對象。
使用Connection
對象對用戶表進行增刪改查。
項目類型Maven Project,打包方式war(其實jar也能夠,之因此使用war是爲了測試JNDI
)。
<!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- dbcp --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.6.0</version> </dependency> <!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- mysql驅動的jar包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>
路徑resources
目錄下,由於是入門例子,這裏僅給出數據庫鏈接參數和鏈接池基本參數,後面源碼會對配置參數進行詳細說明。另外,數據庫sql
腳本也在該目錄下。
#鏈接基本屬性 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root #-------------鏈接池大小和鏈接超時參數-------------------------------- #初始化鏈接數量:鏈接池啓動時建立的初始化鏈接數量 #默認爲0 initialSize=0 #最大活動鏈接數量:鏈接池在同一時間可以分配的最大活動鏈接的數量, 若是設置爲負數則表示不限制 #默認爲8 maxTotal=8 #最大空閒鏈接:鏈接池中允許保持空閒狀態的最大鏈接數量,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制 #默認爲8 maxIdle=8 #最小空閒鏈接:鏈接池中允許保持空閒狀態的最小鏈接數量,低於這個數量將建立新的鏈接,若是設置爲0則不建立 #注意:timeBetweenEvictionRunsMillis爲正數時,這個參數才能生效。 #默認爲0 minIdle=0 #最大等待時間 #當沒有可用鏈接時,鏈接池等待鏈接被歸還的最大時間(以毫秒計數),超過期間則拋出異常,若是設置爲<=0表示無限等待 #默認-1 maxWaitMillis=-1
項目中編寫了JDBCUtils
來初始化鏈接池、獲取鏈接、管理事務和釋放資源等,具體參見項目源碼。
路徑:cn.zzs.dbcp
// 導入配置文件 Properties properties = new Properties(); InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("dbcp.properties"); properties.load(in); // 根據配置文件內容得到數據源對象 DataSource dataSource = BasicDataSourceFactory.createDataSource(properties); // 得到鏈接 Connection conn = dataSource.getConnection();
這裏以保存用戶爲例,路徑test目錄下的cn.zzs.dbcp
。
@Test public void save() { // 建立sql String sql = "insert into demo_user values(null,?,?,?,?,?)"; Connection connection = null; PreparedStatement statement = null; try { // 得到鏈接 connection = JDBCUtils.getConnection(); // 開啓事務設置非自動提交 JDBCUtils.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(); // 提交事務 JDBCUtils.commit(); } catch(Exception e) { JDBCUtils.rollback(); log.error("保存用戶失敗", e); } finally { // 釋放資源 JDBCUtils.release(connection, statement, null); } }
這部份內容從網上參照過來,一樣的內容發的處處都是,暫時沒找到出處。由於內容太過雜亂,並且最新版本更新了很多內容,因此我花了好大功夫才改好,後面找到出處再補上參考資料吧。
注意,這裏在url
後面拼接了多個參數用於避免亂碼、時區報錯問題。 補充下,若是不想加入時區的參數,能夠在mysql
命令窗口執行以下命令:set global time_zone='+8:00'
。
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root
這幾個參數都比較經常使用,具體設置多少需根據項目調整。
#-------------鏈接池大小和鏈接超時參數-------------------------------- #初始化鏈接數量:鏈接池啓動時建立的初始化鏈接數量 #默認爲0 initialSize=0 #最大活動鏈接數量:鏈接池在同一時間可以分配的最大活動鏈接的數量, 若是設置爲負數則表示不限制 #默認爲8 maxTotal=8 #最大空閒鏈接:鏈接池中允許保持空閒狀態的最大鏈接數量,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制 #默認爲8 maxIdle=8 #最小空閒鏈接:鏈接池中允許保持空閒狀態的最小鏈接數量,低於這個數量將建立新的鏈接,若是設置爲0則不建立 #注意:timeBetweenEvictionRunsMillis爲正數時,這個參數才能生效。 #默認爲0 minIdle=0 #最大等待時間 #當沒有可用鏈接時,鏈接池等待鏈接被歸還的最大時間(以毫秒計數),超過期間則拋出異常,若是設置爲<=0表示無限等待 #默認-1 maxWaitMillis=-1 #鏈接池建立的鏈接的默認的數據庫名,若是是使用DBCP的XA鏈接必須設置,否則註冊不了多個資源管理器 #defaultCatalog=github_demo #鏈接池建立的鏈接的默認的schema。若是是mysql,這個設置沒什麼用。 #defaultSchema=github_demo
緩存語句在mysql
下建議關閉。
#-------------緩存語句-------------------------------- #是否緩存preparedStatement,也就是PSCache。 #PSCache對支持遊標的數據庫性能提高巨大,好比說oracle。在mysql下建議關閉 #默認爲false poolPreparedStatements=false #緩存PreparedStatements的最大個數 #默認爲-1 #注意:poolPreparedStatements爲true時,這個參數纔有效 maxOpenPreparedStatements=-1 #緩存read-only和auto-commit狀態。設置爲true的話,全部鏈接的狀態都會是同樣的。 #默認是true cacheState=true
針對鏈接失效和鏈接泄露的問題,建議開啓testWhileIdle
,而不是開啓testOnReturn
或testOnBorrow
(從性能考慮)。
#-------------鏈接檢查狀況-------------------------------- #經過SQL查詢檢測鏈接,注意必須返回至少一行記錄 #默認爲空。即會調用Connection的isValid和isClosed進行檢測 #注意:若是是oracle數據庫的話,應該改成select 1 from dual validationQuery=select 1 from dual #SQL檢驗超時時間 validationQueryTimeout=-1 #是否從池中取出鏈接前進行檢驗。 #默認爲true testOnBorrow=true #是否在歸還到池中前進行檢驗 #默認爲false testOnReturn=false #是否開啓空閒資源回收器。 #默認爲false testWhileIdle=false #空閒資源的檢測週期(單位爲毫秒)。 #默認-1。即空閒資源回收器不工做。 timeBetweenEvictionRunsMillis=-1 #作空閒資源回收器時,每次的採樣數。 #默認3,單位毫秒。若是設置爲-1,就是對全部鏈接作空閒監測。 numTestsPerEvictionRun=3 #資源池中資源最小空閒時間(單位爲毫秒),達到此值後將被移除。 #默認值1000*60*30 = 30分鐘 minEvictableIdleTimeMillis=1800000 #資源池中資源最小空閒時間(單位爲毫秒),達到此值後將被移除。可是會保證minIdle #默認值-1 #softMinEvictableIdleTimeMillis=-1 #空閒資源回收策略 #默認org.apache.commons.pool2.impl.DefaultEvictionPolicy #若是要自定義的話,須要實現EvictionPolicy重寫evict方法 evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy #鏈接最大存活時間。非正數表示不限制 #默認-1 maxConnLifetimeMillis=-1 #當達到maxConnLifetimeMillis被關閉時,是否打印相關消息 #默認true #注意:maxConnLifetimeMillis設置爲正數時,這個參數纔有效 logExpiredConnections=true
這裏的參數主要和事務相關,通常默認就行。
#-------------事務相關的屬性-------------------------------- #鏈接池建立的鏈接的默認的auto-commit狀態 #默認爲空,由驅動決定 defaultAutoCommit=true #鏈接池建立的鏈接的默認的read-only狀態。 #默認值爲空,由驅動決定 defaultReadOnly=false #鏈接池建立的鏈接的默認的TransactionIsolation狀態 #可用值爲下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE #默認值爲空,由驅動決定 defaultTransactionIsolation=REPEATABLE_READ #歸還鏈接時是否設置自動提交爲true #默認true autoCommitOnReturn=true #歸還鏈接時是否設置回滾事務 #默認true rollbackOnReturn=true
當咱們從鏈接池得到了鏈接對象,但由於疏忽或其餘緣由沒有close
,這個時候這個鏈接對象就是一個泄露資源。經過配置如下參數能夠回收這部分對象。
#-------------鏈接泄漏回收參數-------------------------------- #當未使用的時間超過removeAbandonedTimeout時,是否視該鏈接爲泄露鏈接並刪除(當getConnection()被調用時檢測) #默認爲false #注意:這個機制在(getNumIdle() < 2) and (getNumActive() > (getMaxActive() - 3))時被觸發 removeAbandonedOnBorrow=false #當未使用的時間超過removeAbandonedTimeout時,是否視該鏈接爲泄露鏈接並刪除(空閒evictor檢測) #默認爲false #注意:當空閒資源回收器開啓才生效 removeAbandonedOnMaintenance=false #泄露的鏈接能夠被刪除的超時值, 單位秒 #默認爲300 removeAbandonedTimeout=300 #標記當Statement或鏈接被泄露時是否打印程序的stack traces日誌。 #默認爲false logAbandoned=true #這個不是很懂 #默認爲false abandonedUsageTracking=false
這部分參數比較少用。
#-------------其餘-------------------------------- #是否使用快速失敗機制 #默認爲空,由驅動決定 fastFailValidation=false #當使用快速失敗機制時,設置觸發的異常碼 #多個code用","隔開 #disconnectionSqlCodes #borrow鏈接的順序 #默認true lifo=true #每一個鏈接建立時執行的語句 #connectionInitSqls= #鏈接參數:例如username、password、characterEncoding等均可以在這裏設置 #多個參數用";"隔開 #connectionProperties= #指定數據源的jmx名。注意,配置了才能註冊MBean jmxName=cn.zzs.jmx:type=BasicDataSource,name=zzs001 #查詢超時時間 #默認爲空,即根據驅動設置 #defaultQueryTimeout= #控制PoolGuard是否允許獲取底層鏈接 #默認爲false accessToUnderlyingConnectionAllowed=false #若是允許則可使用下面的方式來獲取底層物理鏈接: # Connection conn = ds.getConnection(); # Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate(); # ... # conn.close();
注意:考慮篇幅和可讀性,如下代碼通過刪減,僅保留所需部分。
研究以前,先來看下BasicDataSource
的UML
圖:
這裏介紹下這幾個類的做用:
類名 | 描述 |
---|---|
BasicDataSource |
用於知足基本數據庫操做需求的數據源 |
BasicManagedDataSource |
BasicDataSource 的子類,用於建立支持XA 事務或JTA 事務的鏈接 |
PoolingDataSource |
BasicDataSource 中實際調用的數據源,能夠說BasicDataSource 只是封裝了PoolingDataSource |
ManagedDataSource |
PoolingDataSource 的子類,用於支持XA 事務或JTA 事務的鏈接。是BasicManagedDataSource 中實際調用的數據源,能夠說BasicManagedDataSource 只是封裝了ManagedDataSource |
另外,爲了支持JNDI
,DBCP
也提供了相應的類。
類名 | 描述 |
---|---|
InstanceKeyDataSource |
用於支持JDNI 環境的數據源 |
PerUserPoolDataSource |
InstanceKeyDataSource 的子類,針對每一個用戶會單獨分配一個鏈接池,每一個鏈接池能夠設置不一樣屬性。例如如下需求,相比user,admin 能夠建立更多地鏈接以保證 |
SharedPoolDataSource |
InstanceKeyDataSource 的子類,不一樣用戶共享一個鏈接池 |
本文的源碼分析僅會涉及到BasicDataSource
(包含它封裝的PoolingDataSource
),其餘的數據源暫時不擴展。
BasicDataSourceFactory
只是簡單地new
了一個BasicDataSource
對象並初始化配置參數,此時真正的數據源(PoolingDataSource
)以及鏈接池(GenericObjectPool<PoolableConnection>
)並無建立,而建立的時機爲咱們第一次調用getConnection()
的時候。所以,本文直接從BasicDataSource
的getConnection()
方法開始分析。
public Connection getConnection() throws SQLException { return createDataSource().getConnection(); }
這個方法會建立數據源和鏈接池,整個過程能夠歸納爲如下幾步:
MBean
,用於支持JMX
;GenericObjectPool<PoolableConnection>
;PoolingDataSource<PoolableConnection>
;timeBetweenEvictionRunsMillis
爲正數)。protected DataSource createDataSource() throws SQLException { if(closed) { throw new SQLException("Data source is closed"); } if(dataSource != null) { return dataSource; } synchronized(this) { if(dataSource != null) { return dataSource; } // 註冊MBean,用於支持JMX,這方面的內容不在這裏擴展 jmxRegister(); // 建立原生Connection工廠:本質就是持有數據庫驅動對象和幾個鏈接參數 final ConnectionFactory driverConnectionFactory = createConnectionFactory(); // 將driverConnectionFactory包裝成池化Connection工廠 PoolableConnectionFactory poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory); // 設置PreparedStatements緩存(其實在這裏能夠發現,上面建立池化工廠時就設置了緩存,這裏不必再設置一遍) poolableConnectionFactory.setPoolStatements(poolPreparedStatements); poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements); // 建立數據庫鏈接池對象GenericObjectPool,用於管理鏈接 // BasicDataSource將持有GenericObjectPool對象 createConnectionPool(poolableConnectionFactory); // 建立PoolingDataSource對象 // 該對象持有GenericObjectPool對象的引用 DataSource newDataSource = createDataSourceInstance(); newDataSource.setLogWriter(logWriter); // 根據咱們設置的initialSize建立初始鏈接 for(int i = 0; i < initialSize; i++) { connectionPool.addObject(); } // 開啓鏈接池的evictor線程 startPoolMaintenance(); // 最後BasicDataSource將持有上面建立的PoolingDataSource對象 dataSource = newDataSource; return dataSource; } }
以上方法涉及到幾個類,這裏再補充下UML
圖。
類名 | 描述 |
---|---|
DriverConnectionFactory |
用於生成原生的Connection對象 |
PoolableConnectionFactory |
用於生成池化的Connection對象,持有ConnectionFactory 對象的引用 |
GenericObjectPool |
數據庫鏈接池,用於管理鏈接。持有PoolableConnectionFactory 對象的引用 |
上面已經大體分析了數據源和鏈接池對象的獲取過程,接下來研究下鏈接對象的獲取。在此以前先了解下DBCP
中幾個Connection
實現類。
類名 | 描述 |
---|---|
DelegatingConnection |
Connection 實現類,是如下幾個類的父類 |
PoolingConnection |
用於包裝原生的Connection ,支持緩存prepareStatement 和prepareCall |
PoolableConnection |
用於包裝原生的PoolingConnection (若是沒有開啓poolPreparedStatements ,則包裝的只是原生Connection ),調用close() 時只是將鏈接還給鏈接池 |
PoolableManagedConnection |
PoolableConnection 的子類,用於包裝ManagedConnection ,支持JTA 和XA 事務 |
ManagedConnection |
用於包裝原生的Connection ,支持JTA 和XA 事務 |
PoolGuardConnectionWrapper |
用於包裝PoolableConnection ,當accessToUnderlyingConnectionAllowed 才能獲取底層鏈接對象。咱們獲取到的就是這個對象 |
另外,這裏先歸納下得到鏈接的整個過程:
removeAbandonedOnBorrow
,達到條件會進行檢測;DriverConnectionFactory
建立原生對象,再經過PoolableConnectionFactory
包裝爲池化對象);testOnBorrow
或者testOnCreate
,會經過工廠校驗鏈接有效性;PoolGuardConnectionWrapper
包裝鏈接對象,並返回給客戶端前面已經說過,BasicDataSource
本質上是調用PoolingDataSource
的方法來獲取鏈接,因此這裏從PoolingDataSource.getConnection()
開始研究。
如下代碼可知,該方法會從鏈接池中「借出」鏈接。
public Connection getConnection() throws SQLException { // 這個泛型C指的是PoolableConnection對象 // 調用的是GenericObjectPool的方法返回PoolableConnection對象,這個方法後面會展開 final C conn = pool.borrowObject(); if (conn == null) { return null; } // 包裝PoolableConnection對象,當accessToUnderlyingConnectionAllowed爲true時,可使用底層鏈接 return new PoolGuardConnectionWrapper<>(conn); }
GenericObjectPool
是一個很簡練的類,裏面涉及到的屬性設置和鎖機制都涉及得很是巧妙。
// 存放着鏈接池全部的鏈接對象(但不包含已經釋放的) private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>(); // 存放着空閒鏈接對象的阻塞隊列 private final LinkedBlockingDeque<PooledObject<T>> idleObjects; // 爲n>1表示當前有n個線程正在建立新鏈接對象 private long makeObjectCount = 0; // 建立鏈接對象時所用的鎖 private final Object makeObjectCountLock = new Object(); // 鏈接對象建立總數量 private final AtomicLong createCount = new AtomicLong(0); public T borrowObject() throws Exception { // 若是咱們設置了鏈接獲取等待時間,「借出」過程就必須在指定時間內完成 return borrowObject(getMaxWaitMillis()); } public T borrowObject(final long borrowMaxWaitMillis) throws Exception { // 校驗鏈接池是否打開狀態 assertOpen(); // 若是設置了removeAbandonedOnBorrow,達到觸發條件是會遍歷全部鏈接,未使用時長超過removeAbandonedTimeout的將被釋放掉(通常能夠檢測出泄露鏈接) final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) && (getNumActive() > getMaxTotal() - 3) ) { removeAbandoned(ac); } PooledObject<T> p = null; // 鏈接數達到maxTotal是否阻塞等待 final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; final long waitTime = System.currentTimeMillis(); // 若是獲取的鏈接對象爲空,會再次進入獲取 while (p == null) { create = false; // 獲取空閒隊列的第一個元素,若是爲空就試圖建立新鏈接 p = idleObjects.pollFirst(); if (p == null) { // 後面分析這個方法 p = create(); if (p != null) { create = true; } } // 鏈接數達到maxTotal且暫時沒有空閒鏈接,這時須要阻塞等待,直到得到空閒隊列中的鏈接或等待超時 if (blockWhenExhausted) { if (p == null) { if (borrowMaxWaitMillis < 0) { // 無限等待 p = idleObjects.takeFirst(); } else { // 等待maxWaitMillis p = idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); } } // 這個時候仍是沒有就只能拋出異常 if (p == null) { throw new NoSuchElementException( "Timeout waiting for idle object"); } } else { if (p == null) { throw new NoSuchElementException("Pool exhausted"); } } // 若是鏈接處於空閒狀態,會修改鏈接的state、lastBorrowTime、lastUseTime、borrowedCount等,並返回true if (!p.allocate()) { p = null; } if (p != null) { // 利用工廠從新初始化鏈接對象,這裏會去校驗鏈接存活時間、設置lastUsedTime、及其餘初始參數 try { factory.activateObject(p); } catch (final Exception e) { try { destroy(p); } catch (final Exception e1) { // Ignore - activation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to activate object"); nsee.initCause(e); throw nsee; } } // 根據設置的參數,判斷是否檢測鏈接有效性 if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) { boolean validate = false; Throwable validationThrowable = null; try { // 這裏會去校驗鏈接的存活時間是否超過maxConnLifetimeMillis,以及經過SQL去校驗執行時間 validate = factory.validateObject(p); } catch (final Throwable t) { PoolUtils.checkRethrow(t); validationThrowable = t; } // 若是校驗不經過,會釋放該對象 if (!validate) { try { destroy(p); destroyedByBorrowValidationCount.incrementAndGet(); } catch (final Exception e) { // Ignore - validation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to validate object"); nsee.initCause(validationThrowable); throw nsee; } } } } } // 更新borrowedCount、idleTimes和waitTimes updateStatsBorrow(p, System.currentTimeMillis() - waitTime); return p.getObject(); }
這裏在建立鏈接對象時採用的鎖機制很是值得學習,簡練且高效。
private PooledObject<T> create() throws Exception { int localMaxTotal = getMaxTotal(); if (localMaxTotal < 0) { localMaxTotal = Integer.MAX_VALUE; } final long localStartTimeMillis = System.currentTimeMillis(); final long localMaxWaitTimeMillis = Math.max(getMaxWaitMillis(), 0); // 建立標識: // - TRUE: 調用工廠建立返回對象 // - FALSE: 直接返回null // - null: 繼續循環 Boolean create = null; while (create == null) { synchronized (makeObjectCountLock) { final long newCreateCount = createCount.incrementAndGet(); if (newCreateCount > localMaxTotal) { // 當前池已經達到maxTotal,或者有另一個線程正在試圖建立一個新的鏈接使之達到容量極限 createCount.decrementAndGet(); if (makeObjectCount == 0) { // 鏈接池確實已達到容量極限 create = Boolean.FALSE; } else { // 當前另一個線程正在試圖建立一個新的鏈接使之達到容量極限,此時須要等待 makeObjectCountLock.wait(localMaxWaitTimeMillis); } } else { // 當前鏈接池容量未到達極限,能夠繼續建立鏈接對象 makeObjectCount++; create = Boolean.TRUE; } } // 當達到maxWaitTimeMillis時不建立鏈接對象,直接退出循環 if (create == null && (localMaxWaitTimeMillis > 0 && System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) { create = Boolean.FALSE; } } if (!create.booleanValue()) { return null; } final PooledObject<T> p; try { // 調用工廠建立對象,後面對這個方法展開分析 p = factory.makeObject(); } catch (final Throwable e) { createCount.decrementAndGet(); throw e; } finally { synchronized (makeObjectCountLock) { // 建立標識-1 makeObjectCount--; // 喚醒makeObjectCountLock鎖住的對象 makeObjectCountLock.notifyAll(); } } final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getLogAbandoned()) { p.setLogAbandoned(true); // TODO: in 3.0, this can use the method defined on PooledObject if (p instanceof DefaultPooledObject<?>) { ((DefaultPooledObject<T>) p).setRequireFullStackTrace(ac.getRequireFullStackTrace()); } } // 鏈接數量+1 createdCount.incrementAndGet(); // 將建立的對象放入allObjects allObjects.put(new IdentityWrapper<>(p.getObject()), p); return p; }
public PooledObject<PoolableConnection> makeObject() throws Exception { // 建立原生的Connection對象 Connection conn = connectionFactory.createConnection(); if (conn == null) { throw new IllegalStateException("Connection factory returned null from createConnection"); } try { // 執行咱們設置的connectionInitSqls initializeConnection(conn); } catch (final SQLException sqle) { // Make sure the connection is closed try { conn.close(); } catch (final SQLException ignore) { // ignore } // Rethrow original exception so it is visible to caller throw sqle; } // 鏈接索引+1 final long connIndex = connectionIndex.getAndIncrement(); // 若是設置了poolPreparedStatements,則建立包裝鏈接爲PoolingConnection對象 if (poolStatements) { conn = new PoolingConnection(conn); final GenericKeyedObjectPoolConfig<DelegatingPreparedStatement> config = new GenericKeyedObjectPoolConfig<>(); config.setMaxTotalPerKey(-1); config.setBlockWhenExhausted(false); config.setMaxWaitMillis(0); config.setMaxIdlePerKey(1); config.setMaxTotal(maxOpenPreparedStatements); if (dataSourceJmxObjectName != null) { final StringBuilder base = new StringBuilder(dataSourceJmxObjectName.toString()); base.append(Constants.JMX_CONNECTION_BASE_EXT); base.append(Long.toString(connIndex)); config.setJmxNameBase(base.toString()); config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX); } else { config.setJmxEnabled(false); } final PoolingConnection poolingConn = (PoolingConnection) conn; final KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> stmtPool = new GenericKeyedObjectPool<>( poolingConn, config); poolingConn.setStatementPool(stmtPool); poolingConn.setCacheState(cacheState); } // 用於註冊鏈接到JMX ObjectName connJmxName; if (dataSourceJmxObjectName == null) { connJmxName = null; } else { connJmxName = new ObjectName( dataSourceJmxObjectName.toString() + Constants.JMX_CONNECTION_BASE_EXT + connIndex); } // 建立PoolableConnection對象 final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes, fastFailValidation); pc.setCacheState(cacheState); // 包裝成鏈接池所需的對象 return new DefaultPooledObject<>(pc); }
以上基本已分析完鏈接對象的獲取過程,下面再研究下空閒對象回收器。前面已經講到當建立完數據源對象時會開啓鏈接池的evictor
線程,因此咱們從BasicDataSource.startPoolMaintenance()
開始分析。
前面說過timeBetweenEvictionRunsMillis
爲非正數時不會開啓開啓空閒對象回收器,從如下代碼能夠理解具體邏輯。
protected void startPoolMaintenance() { // 只有timeBetweenEvictionRunsMillis爲正數,纔會開啓空閒對象回收器 if (connectionPool != null && timeBetweenEvictionRunsMillis > 0) { connectionPool.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); } }
這個BaseGenericObjectPool
是上面說到的GenericObjectPool
的父類。
public final void setTimeBetweenEvictionRunsMillis( final long timeBetweenEvictionRunsMillis) { // 設置回收線程運行間隔時間 this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; // 繼續調用本類的方法,下面繼續進入方法分析 startEvictor(timeBetweenEvictionRunsMillis); }
這裏會去定義一個Evictor
對象,這個實際上是一個Runnable
對象,後面會講到。
final void startEvictor(final long delay) { synchronized (evictionLock) { if (null != evictor) { EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS); evictor = null; evictionIterator = null; } // 建立回收器任務,並執行定時調度 if (delay > 0) { evictor = new Evictor(); EvictionTimer.schedule(evictor, delay, delay); } } }
DBCP
是使用ScheduledThreadPoolExecutor
來實現回收器的定時檢測。 涉及到ThreadPoolExecutor
爲JDK
自帶的api
,這裏再也不深刻分析線程池如何實現定時調度。感興趣的朋友能夠複習下經常使用的幾款線程池。
static synchronized void schedule( final BaseGenericObjectPool<?>.Evictor task, final long delay, final long period) if (null == executor) { // 建立線程池,隊列爲DelayedWorkQueue,corePoolSize爲1,maximumPoolSize爲無限大 executor = new ScheduledThreadPoolExecutor(1, new EvictorThreadFactory()); // 當任務被取消的同時從等待隊列中移除 executor.setRemoveOnCancelPolicy(true); } // 設置任務定時調度 final ScheduledFuture<?> scheduledFuture = executor.scheduleWithFixedDelay(task, delay, period, TimeUnit.MILLISECONDS); task.setScheduledFuture(scheduledFuture); }
Evictor
是BaseGenericObjectPool
的內部類,實現了Runnable
接口,這裏看下它的run方法。
class Evictor implements Runnable { private ScheduledFuture<?> scheduledFuture; @Override public void run() { final ClassLoader savedClassLoader = Thread.currentThread().getContextClassLoader(); try { // 確保回收器使用的類加載器和工廠對象的同樣 if (factoryClassLoader != null) { final ClassLoader cl = factoryClassLoader.get(); if (cl == null) { cancel(); return; } Thread.currentThread().setContextClassLoader(cl); } try { // 回收符合條件的對象,後面繼續擴展 evict(); } catch(final Exception e) { swallowException(e); } catch(final OutOfMemoryError oome) { // Log problem but give evictor thread a chance to continue // in case error is recoverable oome.printStackTrace(System.err); } try { // 確保最小空閒對象 ensureMinIdle(); } catch (final Exception e) { swallowException(e); } } finally { Thread.currentThread().setContextClassLoader(savedClassLoader); } } void setScheduledFuture(final ScheduledFuture<?> scheduledFuture) { this.scheduledFuture = scheduledFuture; } void cancel() { scheduledFuture.cancel(false); } }
這裏的回收過程包括如下四道校驗:
按照evictionPolicy
校驗idleSoftEvictTime
、idleEvictTime
;
利用工廠從新初始化樣本,這裏會校驗maxConnLifetimeMillis
(testWhileIdle
爲true);
校驗maxConnLifetimeMillis
和validationQueryTimeout
(testWhileIdle
爲true);
校驗全部鏈接的未使用時間是否超過removeAbandonedTimeout
(removeAbandonedOnMaintenance
爲true)。
public void evict() throws Exception { // 校驗當前鏈接池是否關閉 assertOpen(); if (idleObjects.size() > 0) { PooledObject<T> underTest = null; // 介紹參數時已經講到,這個evictionPolicy咱們能夠自定義 final EvictionPolicy<T> evictionPolicy = getEvictionPolicy(); synchronized (evictionLock) { final EvictionConfig evictionConfig = new EvictionConfig( getMinEvictableIdleTimeMillis(), getSoftMinEvictableIdleTimeMillis(), getMinIdle()); final boolean testWhileIdle = getTestWhileIdle(); // 獲取咱們指定的樣本數,並開始遍歷 for (int i = 0, m = getNumTests(); i < m; i++) { if (evictionIterator == null || !evictionIterator.hasNext()) { evictionIterator = new EvictionIterator(idleObjects); } if (!evictionIterator.hasNext()) { // Pool exhausted, nothing to do here return; } try { underTest = evictionIterator.next(); } catch (final NoSuchElementException nsee) { // 當前樣本正被另外一個線程借出 i--; evictionIterator = null; continue; } // 判斷若是樣本是空閒狀態,設置爲EVICTION狀態 // 若是不是,說明另外一個線程已經借出了這個樣本 if (!underTest.startEvictionTest()) { i--; continue; } boolean evict; try { // 調用回收策略來判斷是否回收該樣本,按照默認策略,如下狀況都會返回true: // 1. 樣本空閒時間大於咱們設置的idleSoftEvictTime,且當前池中空閒鏈接數量>minIdle // 2. 樣本空閒時間大於咱們設置的idleEvictTime evict = evictionPolicy.evict(evictionConfig, underTest, idleObjects.size()); } catch (final Throwable t) { PoolUtils.checkRethrow(t); swallowException(new Exception(t)); evict = false; } // 若是須要回收,則釋放這個樣本 if (evict) { destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } else { // 若是設置了testWhileIdle,會 if (testWhileIdle) { boolean active = false; try { // 利用工廠從新初始化樣本,這裏會校驗maxConnLifetimeMillis factory.activateObject(underTest); active = true; } catch (final Exception e) { // 拋出異常標識校驗不經過,釋放樣本 destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } if (active) { // 接下來會校驗maxConnLifetimeMillis和validationQueryTimeout if (!factory.validateObject(underTest)) { destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } else { try { // 這裏會將樣本rollbackOnReturn、autoCommitOnReturn等 factory.passivateObject(underTest); } catch (final Exception e) { destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } } } } // 若是狀態爲EVICTION或EVICTION_RETURN_TO_HEAD,修改成IDLE if (!underTest.endEvictionTest(idleObjects)) { //空 } } } } } // 校驗全部鏈接的未使用時間是否超過removeAbandonedTimeout final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnMaintenance()) { removeAbandoned(ac); } }
以上已基本研究完數據源建立、鏈接對象獲取和空閒資源回收器,後續有空再作補充。
本文測試使用JNDI
獲取PerUserPoolDataSource
和SharedPoolDataSource
對象,選擇使用tomcat 9.0.21
做容器。
若是以前沒有接觸過JNDI
,並不會影響下面例子的理解,其實能夠理解爲像spring
的bean
配置和獲取。
源碼分析時已經講到,除了咱們熟知的BasicDataSource
,DBCP
還提供了經過JDNI
獲取數據源,以下表。
類名 | 描述 |
---|---|
InstanceKeyDataSource |
用於支持JDNI 環境的數據源,是如下兩個類的父類 |
PerUserPoolDataSource |
InstanceKeyDataSource 的子類,針對每一個用戶會單獨分配一個鏈接池,每一個鏈接池能夠設置不一樣屬性。例如如下需求,相比user,admin 能夠建立更多地鏈接以保證 |
SharedPoolDataSource |
InstanceKeyDataSource 的子類,不一樣用戶共享一個鏈接池 |
本文在前面例子的基礎上增長如下依賴,由於是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
節點。其中bean/DriverAdapterCPDS
這個對象須要被另外兩個使用到。
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="bean/SharedPoolDataSourceFactory" auth="Container" type="org.apache.commons.dbcp2.datasources.SharedPoolDataSource" factory="org.apache.commons.dbcp2.datasources.SharedPoolDataSourceFactory" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" username="root" password="root" maxTotal="8" maxIdle="10" dataSourceName="java:comp/env/bean/DriverAdapterCPDS" /> <Resource name="bean/PerUserPoolDataSourceFactory" auth="Container" type="org.apache.commons.dbcp2.datasources.PerUserPoolDataSource" factory="org.apache.commons.dbcp2.datasources.PerUserPoolDataSourceFactory" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" username="root" password="root" maxTotal="8" maxIdle="10" dataSourceName="java:comp/env/bean/DriverAdapterCPDS" /> <Resource name="bean/DriverAdapterCPDS" auth="Container" type="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS" factory="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" userName="root" userPassword="root" maxIdle="10" /> </Context>
在web-app
節點下配置資源引用,每一個resource-env-ref
指向了咱們配置好的對象。
<resource-env-ref> <description>Test DriverAdapterCPDS</description> <resource-env-ref-name>bean/DriverAdapterCPDS</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS</resource-env-ref-type> </resource-env-ref> <resource-env-ref> <description>Test SharedPoolDataSource</description> <resource-env-ref-name>bean/SharedPoolDataSourceFactory</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.datasources.SharedPoolDataSource</resource-env-ref-type> </resource-env-ref> <resource-env-ref> <description>Test erUserPoolDataSource</description> <resource-env-ref-name>bean/erUserPoolDataSourceFactory</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.datasources.erUserPoolDataSource</resource-env-ref-type> </resource-env-ref>
由於須要在web
環境中使用,若是直接建類寫個main
方法測試,會一直報錯的,目前沒找到好的辦法。這裏就簡單地使用jsp
來測試吧(這是從tomcat官網參照的例子)。
<body> <% // 得到名稱服務的上下文對象 Context initCtx = new InitialContext(); Context envCtx = (Context)initCtx.lookup("java:comp/env/"); // 查找指定名字的對象 DataSource ds = (DataSource)envCtx.lookup("bean/SharedPoolDataSourceFactory"); DataSource ds2 = (DataSource)envCtx.lookup("bean/PerUserPoolDataSourceFactory"); // 獲取鏈接 Connection conn = ds.getConnection("root","root"); System.out.println("conn" + conn); Connection conn2 = ds2.getConnection("zzf","zzf"); System.out.println("conn2" + conn2); // ... 使用鏈接操做數據庫,以及釋放資源 ... conn.close(); conn2.close(); %> </body>
打包項目在tomcat9
上運行,訪問 http://localhost:8080/DBCP-demo/testInstanceKeyDataSource.jsp ,控制檯打印以下內容:
conn=1971654708, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=root@localhost, MySQL Connector/J conn2=128868782, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=zzf@localhost, MySQL Connector/J
前面源碼分析已經講到,如下類用於支持JTA
事務。本文將介紹如何使用DBCP
來實現JTA
事務兩階段提交(固然,實際項目並不支持使用2PC
,由於性能開銷太大)。
類名 | 描述 |
---|---|
BasicManagedDataSource |
BasicDataSource 的子類,用於建立支持XA 事務或JTA 事務的鏈接 |
ManagedDataSource |
PoolingDataSource 的子類,用於支持XA 事務或JTA 事務的鏈接。是BasicManagedDataSource 中實際調用的數據源,能夠說BasicManagedDataSource 只是封裝了ManagedDataSource |
由於測試例子使用的是mysql
,使用XA
事務須要開啓支持。注意,mysql
只有innoDB
引擎才支持(另外,XA
事務和常規事務是互斥的,若是開啓了XA
事務,其餘線程進來即便只讀也是不行的)。
SHOW VARIABLES LIKE '%xa%' -- 查看XA事務是否開啓 SET innodb_support_xa = ON -- 開啓XA事務
除了原來的github_demo
數據庫,我另外建了一個test
數據庫,簡單地模擬兩個數據庫。
測試以前,這裏簡單回顧下直接使用sql
操做XA
事務的過程,將有助於對如下內容的理解:
XA START 'my_test_xa'; -- 啓動一個xid爲my_test_xa的事務,並使之爲active狀態 UPDATE github_demo.demo_user SET deleted = 1 WHERE id = '1'; -- 事務中的語句 XA END 'my_test_xa'; -- 把事務置爲idle狀態 XA PREPARE 'my_test_xa'; -- 把事務置爲prepare狀態 XA COMMIT 'my_test_xa'; -- 提交事務 XA ROLLBACK 'my_test_xa'; -- 回滾事務 XA RECOVER; -- 查看處於prepare狀態的事務列表
在入門例子的基礎上,增長如下依賴,本文采用第三方atomikos
的實現。
<!-- jta:用於測試DBCP對JTA事務的支持 --> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jdbc</artifactId> <version>3.9.3</version> </dependency>
這裏千萬記得要設置DefaultCatalog
,不然當前事務中註冊不一樣資源管理器時,可能都會被當成同一個資源管理器而拒絕註冊並報錯,由於這個問題,花了我好長時間才解決。
public BasicManagedDataSource getBasicManagedDataSource( TransactionManager transactionManager, String url, String username, String password) { BasicManagedDataSource basicManagedDataSource = new BasicManagedDataSource(); basicManagedDataSource.setTransactionManager(transactionManager); basicManagedDataSource.setUrl(url); basicManagedDataSource.setUsername(username); basicManagedDataSource.setPassword(password); basicManagedDataSource.setDefaultAutoCommit(false); basicManagedDataSource.setXADataSource("com.mysql.cj.jdbc.MysqlXADataSource"); return basicManagedDataSource; } @Test public void test01() throws Exception { // 得到事務管理器 TransactionManager transactionManager = new UserTransactionManager(); // 獲取第一個數據庫的數據源 BasicManagedDataSource basicManagedDataSource1 = getBasicManagedDataSource( transactionManager, "jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", "root", "root"); // 注意,這一步很是重要 basicManagedDataSource1.setDefaultCatalog("github_demo"); // 獲取第二個數據庫的數據源 BasicManagedDataSource basicManagedDataSource2 = getBasicManagedDataSource( transactionManager, "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", "zzf", "zzf"); // 注意,這一步很是重要 basicManagedDataSource1.setDefaultCatalog("test"); }
經過運行代碼能夠發現,當數據庫1和2的操做都成功,纔會提交,只要其中一個數據庫執行失敗,兩個操做都會回滾。
@Test public void test01() throws Exception { Connection connection1 = null; Statement statement1 = null; Connection connection2 = null; Statement statement2 = null; transactionManager.begin(); try { // 獲取鏈接並進行數據庫操做,這裏會將會將XAResource註冊到當前線程的XA事務對象 /** * XA START xid1;-- 啓動一個事務,並使之爲active狀態 */ connection1 = basicManagedDataSource1.getConnection(); statement1 = connection1.createStatement(); /** * update github_demo.demo_user set deleted = 1 where id = '1'; -- 事務中的語句 */ boolean result1 = statement1.execute("update github_demo.demo_user set deleted = 1 where id = '1'"); System.out.println(result1); /** * XA START xid2;-- 啓動一個事務,並使之爲active狀態 */ connection2 = basicManagedDataSource2.getConnection(); statement2 = connection2.createStatement(); /** * update test.demo_user set deleted = 1 where id = '1'; -- 事務中的語句 */ boolean result2 = statement2.execute("update test.demo_user set deleted = 1 where id = '1'"); System.out.println(result2); /** * 當這執行如下語句: * XA END xid1; -- 把事務置爲idle狀態 * XA PREPARE xid1; -- 把事務置爲prepare狀態 * XA END xid2; -- 把事務置爲idle狀態 * XA PREPARE xid2; -- 把事務置爲prepare狀態 * XA COMMIT xid1; -- 提交事務 * XA COMMIT xid2; -- 提交事務 */ transactionManager.commit(); } catch(Exception e) { e.printStackTrace(); } finally { statement1.close(); statement2.close(); connection1.close(); connection2.close(); } }
相關源碼請移步:https://github.com/ZhangZiSheng001/dbcp-demo
本文爲原創文章,轉載請附上原文出處連接:https://www.cnblogs.com/ZhangZiSheng001/p/12003922.html