最近在看一些dbcp的相關內容,順便做一下記錄,免得自己給忘記了。
1. 引入dbcp (選擇1.4)
<dependency>
<groupId>com.alibaba.external</groupId>
<artifactId>jakarta.commons.dbcp</artifactId>
<version>1.4</version>
</dependency>
?
2. dbcp的基本配置
相關配置說明:
?
-
initialSize :連接池啟動時創建的初始化連接數量(默認值為0)
-
maxActive
:連接池中可同時連接的最大的連接數(默認值為8,調整為20,高峰單機器在20并發左右,自己根據應用場景定)
-
maxIdle:連接池中最大的空閑的連接數,超過的空閑連接將被釋放,如果設置為負數表示不限制(默認為8個,maxIdle不能設置太小,因為假如在高負載的情況下,連接的打開時間比關閉的時間快,會引起連接池中idle的個數 上升超過maxIdle,而造成頻繁的連接銷毀和創建,類似于jvm參數中的Xmx設置)
-
minIdle:連接池中最小的空閑的連接數,低于這個數量會被創建新的連接(默認為0,調整為5,該參數越接近maxIdle,性能越好,因為連接的創建和銷毀,都是需要消耗資源的;但是不能太大,因為在機器很空閑的時候,也會創建低于minidle個數的連接,類似于jvm參數中的Xmn設置)
-
maxWait
?:最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,超過該時間限制會拋出異常,如果設置-1表示無限等待(默認為無限,調整為60000ms,避免因線程池不夠用,而導致請求被無限制掛起)
-
poolPreparedStatements:開啟池的prepared(默認是false,未調整,經過測試,開啟后的性能沒有關閉的好。)
-
maxOpenPreparedStatements:開啟池的prepared 后的同時最大連接數(默認無限制,同上,未配置)
-
minEvictableIdleTimeMillis
?:連接池中連接,在時間段內一直空閑, 被逐出連接池的時間
-
(默認為30分鐘,可以適當做調整,需要和后端服務端的策略配置相關)
-
removeAbandonedTimeout
?:超過時間限制,回收沒有用(廢棄)的連接(默認為 300秒,調整為180)
-
removeAbandoned
?:超過removeAbandonedTimeout時間后,是否進 行沒用連接(廢棄)的回收(默認為false,調整為true)
removeAbandoned參數解釋:
-
如果開啟了removeAbandoned,當getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)時被觸發.
-
舉例當maxActive=20, 活動連接為18,空閑連接為1時可以觸發"removeAbandoned".但是活動連接只有在沒有被使用的時間超 過"removeAbandonedTimeout"時才被回收
-
logAbandoned: 標記當連接被回收時是否打印程序的stack traces日志(默認為false,未調整)
一般會是幾種情況出現需要
removeAbandoned
:
-
代碼未在
finally
釋放
connection
,
不過我們都用
sqlmapClientTemplate
,底層都有鏈接釋放的過程
-
遇到數據庫死鎖
。以前遇到過后端存儲過程做了鎖表操作,導致前臺集群中連接池全都被
block
住,后續的業務處理因為拿不到鏈接所有都處理失敗了。
一份優化過的配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="xxxx" />
<property name="username"><value>xxxx</value></property>
<property name="password"><value>xxxxx</value></property>
<property name="maxActive"><value>20</value></property>
<property name="initialSize"><value>1</value></property>
<property name="maxWait"><value>60000</value></property>
<property name="maxIdle"><value>20</value></property>
<property name="minIdle"><value>3</value></property>
<property name="removeAbandoned"><value>true</value></property>
<property name="removeAbandonedTimeout"><value>180</value></property>
<property name="connectionProperties"><value>clientEncoding=GBK</value></property>
</bean>
?
?
2. dbcp的鏈接validate配置
-
dbcp是采用了commons-pool做為其連接池管理,testOnBorrow,testOnReturn, testWhileIdle是pool是提供的幾種校驗機制,通過外部鉤子的方式回調dbcp的相關數據庫鏈接(validationQuery)校驗
-
dbcp相關外部鉤子類:PoolableConnectionFactory,繼承于common-pool PoolableObjectFactory
-
dbcp通過GenericObjectPool這一入口,進行連接池的borrow,return處理
-
testOnBorrow : 顧明思義,就是在進行borrowObject進行處理時,對拿到的connection進行validateObject校驗
-
testOnReturn : 顧明思義,就是在進行returnObject對返回的connection進行validateObject校驗,個人覺得對數據庫連接池的管理意義不大
-
testWhileIdle
: 關注的重點,GenericObjectPool中針對pool管理,起了一個Evict的TimerTask定時線程進行控制(可通過設置參數timeBetweenEvictionRunsMillis>0),定時對線程池中的鏈接進行validateObject校驗,對無效的鏈接進行關閉后,會調用ensureMinIdle,適當建立鏈接保證最小的minIdle連接數。
-
timeBetweenEvictionRunsMillis,設置的Evict線程的時間,單位ms,大于0才會開啟evict檢查線程
-
validateQuery, 代表檢查的sql
-
validateQueryTimeout, 代表在執行檢查時,通過statement設置,statement.setQueryTimeout(validationQueryTimeout)
-
numTestsPerEvictionRun,代表每次檢查鏈接的數量,建議設置和maxActive一樣大,這樣每次可以有效檢查所有的鏈接.
?
- initialSize :連接池啟動時創建的初始化連接數量(默認值為0)
- maxActive :連接池中可同時連接的最大的連接數(默認值為8,調整為20,高峰單機器在20并發左右,自己根據應用場景定)
- maxIdle:連接池中最大的空閑的連接數,超過的空閑連接將被釋放,如果設置為負數表示不限制(默認為8個,maxIdle不能設置太小,因為假如在高負載的情況下,連接的打開時間比關閉的時間快,會引起連接池中idle的個數 上升超過maxIdle,而造成頻繁的連接銷毀和創建,類似于jvm參數中的Xmx設置)
- minIdle:連接池中最小的空閑的連接數,低于這個數量會被創建新的連接(默認為0,調整為5,該參數越接近maxIdle,性能越好,因為連接的創建和銷毀,都是需要消耗資源的;但是不能太大,因為在機器很空閑的時候,也會創建低于minidle個數的連接,類似于jvm參數中的Xmn設置)
- maxWait ?:最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,超過該時間限制會拋出異常,如果設置-1表示無限等待(默認為無限,調整為60000ms,避免因線程池不夠用,而導致請求被無限制掛起)
- poolPreparedStatements:開啟池的prepared(默認是false,未調整,經過測試,開啟后的性能沒有關閉的好。)
- maxOpenPreparedStatements:開啟池的prepared 后的同時最大連接數(默認無限制,同上,未配置)
- minEvictableIdleTimeMillis ?:連接池中連接,在時間段內一直空閑, 被逐出連接池的時間
- (默認為30分鐘,可以適當做調整,需要和后端服務端的策略配置相關)
- removeAbandonedTimeout ?:超過時間限制,回收沒有用(廢棄)的連接(默認為 300秒,調整為180)
- removeAbandoned ?:超過removeAbandonedTimeout時間后,是否進 行沒用連接(廢棄)的回收(默認為false,調整為true)
- 如果開啟了removeAbandoned,當getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)時被觸發.
- 舉例當maxActive=20, 活動連接為18,空閑連接為1時可以觸發"removeAbandoned".但是活動連接只有在沒有被使用的時間超 過"removeAbandonedTimeout"時才被回收
- logAbandoned: 標記當連接被回收時是否打印程序的stack traces日志(默認為false,未調整)
- 代碼未在 finally 釋放 connection , 不過我們都用 sqlmapClientTemplate ,底層都有鏈接釋放的過程
- 遇到數據庫死鎖 。以前遇到過后端存儲過程做了鎖表操作,導致前臺集群中連接池全都被 block 住,后續的業務處理因為拿不到鏈接所有都處理失敗了。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="xxxx" />
<property name="username"><value>xxxx</value></property>
<property name="password"><value>xxxxx</value></property>
<property name="maxActive"><value>20</value></property>
<property name="initialSize"><value>1</value></property>
<property name="maxWait"><value>60000</value></property>
<property name="maxIdle"><value>20</value></property>
<property name="minIdle"><value>3</value></property>
<property name="removeAbandoned"><value>true</value></property>
<property name="removeAbandonedTimeout"><value>180</value></property>
<property name="connectionProperties"><value>clientEncoding=GBK</value></property>
</bean>
?
- dbcp是采用了commons-pool做為其連接池管理,testOnBorrow,testOnReturn, testWhileIdle是pool是提供的幾種校驗機制,通過外部鉤子的方式回調dbcp的相關數據庫鏈接(validationQuery)校驗
- dbcp相關外部鉤子類:PoolableConnectionFactory,繼承于common-pool PoolableObjectFactory
- dbcp通過GenericObjectPool這一入口,進行連接池的borrow,return處理
- testOnBorrow : 顧明思義,就是在進行borrowObject進行處理時,對拿到的connection進行validateObject校驗
- testOnReturn : 顧明思義,就是在進行returnObject對返回的connection進行validateObject校驗,個人覺得對數據庫連接池的管理意義不大
- testWhileIdle : 關注的重點,GenericObjectPool中針對pool管理,起了一個Evict的TimerTask定時線程進行控制(可通過設置參數timeBetweenEvictionRunsMillis>0),定時對線程池中的鏈接進行validateObject校驗,對無效的鏈接進行關閉后,會調用ensureMinIdle,適當建立鏈接保證最小的minIdle連接數。
-
timeBetweenEvictionRunsMillis,設置的Evict線程的時間,單位ms,大于0才會開啟evict檢查線程
- validateQuery, 代表檢查的sql
- validateQueryTimeout, 代表在執行檢查時,通過statement設置,statement.setQueryTimeout(validationQueryTimeout)
- numTestsPerEvictionRun,代表每次檢查鏈接的數量,建議設置和maxActive一樣大,這樣每次可以有效檢查所有的鏈接.
<property name="testWhileIdle"><value>true</value></property> <!-- 打開檢查,用異步線程evict進行檢查 -->
<property name="testOnBorrow"><value>false</value></property>
<property name="testOnReturn"><value>false</value></property>
<property name="validationQuery"><value>select sysdate from dual</value></property>
<property name="validationQueryTimeout"><value>1</value></property>
<property name="timeBetweenEvictionRunsMillis"><value>30000</value></property>
<property name="numTestsPerEvictionRun"><value>20</value></property>
?相關配置需求:
?
- 目前網站的應用大部分的瓶頸還是在I/O這一塊,大部分的I/O還是在數據庫的這一層面上,每一個請求可能會調用10來次SQL查詢,如果不走事務,一個請求會重復獲取鏈接,如果每次獲取鏈接都進行validateObject,性能開銷不是很能接受,可以假定一次SQL操作消毫0.5~1ms(一般走了網絡請求基本就這數)
- 網站異常數據庫重啟,網絡異常斷開的頻率是非常低的,一般也就在數據庫升級,演習維護時才會進行,而且一般也是選在晚上,訪問量相對比較低的請求,而且一般會有人員值班關注,所以異步的validateObject是可以接受,但一個前提需要確保能保證在一個合理的時間段內,數據庫能完成自動重聯。
?
public interface PoolableObjectFactory {
Object makeObject() throws Exception;
void destroyObject(Object obj) throws Exception;
boolean validateObject(Object obj);
void activateObject(Object obj) throws Exception;
void passivateObject(Object obj) throws Exception;
}
?
?
2. dbcp實現的pool從池管理操作
?
這里貼了一個相關validate代碼,具體類可見:PoolableConnectionFactory.validateConnection()
?
public class PoolableConnectionFactory implements PoolableObjectFactory {
......
public boolean validateObject(Object obj) { //驗證validateObject
if(obj instanceof Connection) {
try {
validateConnection((Connection) obj);
return true;
} catch(Exception e) {
return false;
}
} else {
return false;
}
}
public void validateConnection(Connection conn) throws SQLException {
String query = _validationQuery;
if(conn.isClosed()) {
throw new SQLException("validateConnection: connection closed");
}
if(null != query) {
Statement stmt = null;
ResultSet rset = null;
try {
stmt = conn.createStatement();
if (_validationQueryTimeout > 0) {
stmt.setQueryTimeout(_validationQueryTimeout);
}
rset = stmt.executeQuery(query);
if(!rset.next()) {
throw new SQLException("validationQuery didn't return a row");
}
} finally {
if (rset != null) {
try {
rset.close();
} catch(Exception t) {
// ignored
}
}
if (stmt != null) {
try {
stmt.close();
} catch(Exception t) {
// ignored
}
}
}
}
}
....
}
?
3. pool池的evict調用代碼:GenericObjectPool (apache commons pool version 1.5.4)
protected synchronized void startEvictor(long delay) { //啟動Evictor為TimerTask
if(null != _evictor) {
EvictionTimer.cancel(_evictor);
_evictor = null;
}
if(delay > 0) {
_evictor = new Evictor();
EvictionTimer.schedule(_evictor, delay, delay);
}
}
for (int i=0,m=getNumTests();i<m;i++) {
final ObjectTimestampPair pair;
.......
boolean removeObject = false;
// 空閑鏈接處理
final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp;
if ((getMinEvictableIdleTimeMillis() > 0) &&
(idleTimeMilis > getMinEvictableIdleTimeMillis())) {
removeObject = true;
} else if ((getSoftMinEvictableIdleTimeMillis() > 0) &&
(idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) &&
((getNumIdle() + 1)> getMinIdle())) {
removeObject = true;
}
// testWhileIdle sql 檢查處理
if(getTestWhileIdle() && !removeObject) {
boolean active = false;
try {
_factory.activateObject(pair.value);
active = true;
} catch(Exception e) {
removeObject=true;
}
if(active) {
if(!_factory.validateObject(pair.value)) {
removeObject=true;
} else {
try {
_factory.passivateObject(pair.value);
} catch(Exception e) {
removeObject=true;
}
}
}
}
// 真正關閉
if (removeObject) {
try {
_factory.destroyObject(pair.value);
} catch(Exception e) {
// ignored
}
}
........
?
注意: 目前dbcp的pool的實現是使用了公用的apache common pools進行擴展處理,所以和原生的連接池處理,代碼看上去有點別扭,感覺自動重連這塊異常處理不怎么好,我也就只重點關注了這部分代碼而已?
?.
?
3. dbcp的鏈接自動重鏈相關測試
相關場景:
- 數據庫意外重啟后,原先的數據庫連接池能自動廢棄老的無用的鏈接,建立新的數據庫鏈接
- 網絡異常中斷后,原先的建立的tcp鏈接,應該能進行自動切換
測試需求1步驟
- 建立一testCase代碼
- 配置mysql數據庫
- 循環執行在SQL查詢過程
- 異常重啟mysql數據庫
測試需求2步驟
- 建立一testCase代碼
- 配置mysql數據庫
- 循環執行在SQL查詢過程
-
通過iptables禁用網絡鏈接
/sbin/iptables -A INPUT -s 10.16.2.69 -j REJECT
/sbin/iptables -A FORWARD -p tcp -s 10.16.2.69 --dport 3306 -m state --state NEW,ESTABLISHED -j DROP
???? 5. iptables -F 清空規則,恢復鏈接通道。
?
測試需求問題記錄
?
分別測試了兩種配置,有validateObject的配置和沒有validateObject的相關配置。
1. 沒有validate配置
問題一: 異常重啟mysql數據庫后,居然也可以自動恢復鏈接,sql查詢正常
跟蹤了一下代碼,發現這么一個問題:
- 在數據庫關閉的時候,client中pool通過borrowObject獲取一個異常鏈接返回給client
- client在使用具體的異常鏈接進行sql調用出錯了,拋了異常
- 在finally,調用connection.close(),本意是應該調用pool通過returnObject返回到的池中,但在跟蹤代碼時,未見調用GenericObjectPool的returnObject
- 繼續查,發現在dbcp在中PoolingDataSource(實現DataSource接口)調用PoolableConnection(dbcp pool相關的delegate操作)進行相應關閉時,會檢查_conn.isClosed(),針對DataSource如果isClosed返回為true的則不調用returnObject,直接丟棄了鏈接??
解釋:
- 正因為在獲取異常鏈接后,因為做了_conn.isClosed()判斷,所以異常鏈接并沒有返回到連接池中,所以到數據庫重啟恢復后,每次都是調用pool重新構造一個新的connection,所以后面就正常了
- _conn.isClosed()是否保險,從jdk的api描述中: A connection is closed if the method close has been called on it or if certain fatal errors have occurred. 里面提供兩種情況,一種就是被調用了closed方法,另一種就是出現一些異常也說的比較含糊。
問題二:validateObject調用時,dbcp設置的validationQueryTimeout居然沒效果
看了mysql statement代碼實現,找到了答案。?
mysql com.mysql.jdbc.statemen 部分代碼
?
timeout時間處理:
timeoutTask = new CancelTask();
//通過TimerTask啟動一定時任務
Connection.getCancelTimer().schedule(timeoutTask, this.timeoutInMillis);
?
對應的CancelTask的代碼:?
?
class CancelTask extends TimerTask {
long connectionId = 0;
CancelTask() throws SQLException {
connectionId = connection.getIO().getThreadId();
}
public void run() {
Thread cancelThread = new Thread() {
public void run() {
Connection cancelConn = null;
java.sql.Statement cancelStmt = null;
try {
cancelConn = connection.duplicate();
cancelStmt = cancelConn.createStatement();
// 簡單暴力,再發起一條KILL SQL,關閉先前的sql thread id
cancelStmt.execute("KILL QUERY " + connectionId);
wasCancelled = true;
} catch (SQLException sqlEx) {
throw new RuntimeException(sqlEx.toString());
} finally {
if (cancelStmt != null) {
try {
cancelStmt.close();
} catch (SQLException sqlEx) {
throw new RuntimeException(sqlEx.toString());
}
}
if (cancelConn != null) {
try {
cancelConn.close();
} catch (SQLException sqlEx) {
throw new RuntimeException(sqlEx.toString());
}
}
}
}
};
cancelThread.start();
}
}
?
?
原因總結一句話: queryTimeout的實現是通過底層數據庫提供的機制,比如KILL QUERY pid. ?如果此時的網絡不通,出現阻塞現象,對應的kill命令也發不出去,所以timeout設置的超時沒效果。
4.最后
最后還是決定配置testWhileIdle掃描,主要考慮:
- pool池中的鏈接如果未被使用,可以通過testWhileIdle進行鏈接檢查,避免在使用時后總要失敗那么一次,可以及時預防
- 配合連接池的minEvictableIdleTimeMillis(空閑鏈接),removeAbandoned(未釋放的鏈接),可以更好的去避免因為一些異常情況引起的問題,防范于未然。比如使用一些分布式數據庫的中間件,會有空閑鏈接關閉的動作,動態伸縮連接池,這時候需要能及時的發現,避免請求失敗。
- testOnBorrow個人不太建議使用,存在性能問題,試想一下連接一般會在什么情況出問題,網絡或者服務端異常終端空閑鏈接,網絡中斷你testOnBorrow檢查發現不對再取一個鏈接還是不對,針對空閑鏈接處理異常關閉,可以從好業務端的重試策略進行考慮,同時配置客戶端的空閑鏈接超時時間,maxIdle,minIdle等。
?
--------------------------------------------
新加的內容:
5.dbcp密碼加密處理
以前使用jboss的jndi數據源的方式,是通過配置oracle-ds.xml,可以設置<security-domain>EncryptDBPassword</security-domain>,引用jboss login-config.xml配置的加密配置。
?
?
<application-policy name="EncryptDBPassword">
<authentication>
<login-module code="org.jboss.resource.security.SecureIdentityLoginModule" flag="required">
<module-option name="username">${username}</module-option>
<module-option name="password">${password_encrypt}</module-option>
<module-option name="managedConnectionFactoryName">jboss.jca:service=LocalTxCM,name=${jndiName}</module-option>
</login-module>
</authentication>
</application-policy>
?
?
為了能達到同樣的效果,切換為spring dbcp配置時,也有類似密碼加密的功能,運行期進行密碼decode,最后進行數據鏈接。
?
?
實現方式很簡單,分析jboss的對應 SecureIdentityLoginModule 的實現,無非就是走了Blowfish加密算法,自己拷貝實現一份。
?
?
private static String encode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] kbytes = "jaas is the way".getBytes();
SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
Cipher cipher = Cipher.getInstance("Blowfish");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encoding = cipher.doFinal(secret.getBytes());
BigInteger n = new BigInteger(encoding);
return n.toString(16);
}
private static char[] decode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] kbytes = "jaas is the way".getBytes();
SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
BigInteger n = new BigInteger(secret, 16);
byte[] encoding = n.toByteArray();
Cipher cipher = Cipher.getInstance("Blowfish");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decode = cipher.doFinal(encoding);
return new String(decode).toCharArray();
}
?
最后的配置替換為:
?
?
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
......
<property name="password"><!-- 注意多了一層轉化,將密碼串調用decode解密為最初的數據庫密碼 -->
<bean class="com.xxxxx.EncryptDBPasswordFactory">
<property name="password" value="${xxxx.password.encrypted}" />
</bean>
</property>
........
</bean>
?
--------------------------------------------
新加的內容:
6.數據庫重連機制
常見的問題:
1. 數據庫意外重啟后,原先的數據庫連接池能自動廢棄老的無用的鏈接,建立新的數據庫鏈接
2. 網絡異常中斷后,原先的建立的tcp 鏈接,應該能進行自動切換。比如網站演習中的交換機重啟會導致網絡瞬斷
3. 分布式數據庫中間件,比如amoeba 會定時的將空閑鏈接異常關閉,客戶端會出現半開的空閑鏈接。
?
大致的解決思路:?
1. sql 心跳檢查
? 主動式 ,即我前面提到的sql validate相關配置
2. 請求探雷
??? 犧牲小我,完成大我的精神。 拿鏈接嘗試一下,發現處理失敗丟棄鏈接,探雷的請求總會失敗幾個,就是前面遇到的問題一,dbcp已經支持該功能,不需要額外置。
3. 設置合理的超時時間,
????? 解決半開鏈接. 一般數據庫mysql,oracle都有一定的鏈接空閑斷開的機制,而且當你使用一些分布式中間件(軟件一類的),空閑鏈接控制會更加嚴格,這時候設置合理的超時時間可以有效 避免半開鏈接。
???? 一般超時時間,dbcp主要是minEvictableIdleTimeMillis(空閑鏈接) , removeAbandonedTimeout(鏈接泄漏)。可以見前面的參數解釋。
?
?
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

