持續更新:https://github.com/dchack/Mybatis-source-code-learn
Mybatis連接池
有這麼個定律,有連接的地方就有池。
在市面上,可以適配Mybatis DateSource的連接池有很對,比如:
Mybatis也自帶來連接池的功能,先學習下Mybatis的,相對簡單的實現。
涉及的類:
PoolState
public class PoolState {
protected PooledDataSource dataSource;
// 空閒連接集合
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
// 正在使用的連接集合
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
// 請求次數,每次獲取連接,都會自增,用於
protected long requestCount = 0;
// 累計請求耗時,每次獲取連接時計算累加,除以requestCount可以獲得平均耗時
protected long accumulatedRequestTime = 0;
// 累計連接使用時間
protected long accumulatedCheckoutTime = 0;
// 過期連接次數
protected long claimedOverdueConnectionCount = 0;
protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
// 累計等待獲取連接時間
protected long accumulatedWaitTime = 0;
// 等待獲取連接的次數
protected long hadToWaitCount = 0;
// 連接已關閉的次數
protected long badConnectionCount = 0;
public PoolState(PooledDataSource dataSource) {
this.dataSource = dataSource;
}
public synchronized long getRequestCount() {
return requestCount;
}
public synchronized long getAverageRequestTime() {
return requestCount == 0 ? 0 : accumulatedRequestTime / requestCount;
}
public synchronized long getAverageWaitTime() {
return hadToWaitCount == 0 ? 0 : accumulatedWaitTime / hadToWaitCount;
}
public synchronized long getHadToWaitCount() {
return hadToWaitCount;
}
public synchronized long getBadConnectionCount() {
return badConnectionCount;
}
public synchronized long getClaimedOverdueConnectionCount() {
return claimedOverdueConnectionCount;
}
public synchronized long getAverageOverdueCheckoutTime() {
return claimedOverdueConnectionCount == 0 ? 0 : accumulatedCheckoutTimeOfOverdueConnections / claimedOverdueConnectionCount;
}
public synchronized long getAverageCheckoutTime() {
return requestCount == 0 ? 0 : accumulatedCheckoutTime / requestCount;
}
public synchronized int getIdleConnectionCount() {
return idleConnections.size();
}
public synchronized int getActiveConnectionCount() {
return activeConnections.size();
}
}
注意代碼中的字段都是用protected修飾的,表示pooled包內都可訪問,在寫這份代碼的時候必然默認這個包下實現一個獨立的功能,內部字段都可以共享使用,否則都寫set,get方法太麻煩了。
在PoolState
類中,很多指標比如requestCount
,claimedOverdueConnectionCount
等都不和連接池核心邏輯相關,純粹只是表示連接池的一些指標而已。
作爲連接池,在這裏最重要的就是兩個List:
- idleConnections
- activeConnections
這兩個都是ArrayList,所以在整個實現中我們是通過synchronized
關鍵字來處理併發場景的。
PooledConnection
組成池的兩個List中存儲的是PooledConnection
,而PooledConnection
通過java動態代理機制實現代理真正Connection。
PooledConnection
繼承InvocationHandler
,所以實現了invoke
方法:
/*
* Required for InvocationHandler implementation.
*
* @param proxy - not used
* @param method - the method to be executed
* @param args - the parameters to be passed to the method
* @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
主要看到這個代理實現處理了close
方法,就是將連接從使用列表中彈出。
對於其他方法,會判斷方法是否屬於Object中的方法,如果不是則進行連接合法的校驗,然後執行真正Connection
即realConnection
中對應的方法。
獲得一個代理類的代碼,即調用Proxy.newProxyInstance
方法,在PooledConnection
中的構造函數中:
/*
* Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
*
* @param connection - the connection that is to be presented as a pooled connection
* @param dataSource - the dataSource that the connection is from
*/
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
我們可以看到realConnection
是在構造函數時就傳入的了。
而配置這個池的參數都是在PooledDataSource
中:
官方文檔:
poolMaximumActiveConnections – 在任意時間可以存在的活動(也就是正在使用)連接數量,默認值:10
poolMaximumIdleConnections – 任意時間可能存在的空閒連接數。
poolMaximumCheckoutTime – 在被強制返回之前,池中連接被檢出(checked out)時間,默認值:20000 毫秒(即 20 秒)
poolTimeToWait – 這是一個底層設置,如果獲取連接花費了相當長的時間,連接池會打印狀態日誌並重新嘗試獲取一個連接(避免在誤配置的情況下一直安靜的失敗),默認值:20000 毫秒(即 20 秒)。
poolMaximumLocalBadConnectionTolerance – 這是一個關於壞連接容忍度的底層設置, 作用於每一個嘗試從緩存池獲取連接的線程。 如果這個線程獲取到的是一個壞的連接,那麼這個數據源允許這個線程嘗試重新獲取一個新的連接,但是這個重新嘗試的次數不應該超過 poolMaximumIdleConnections 與 poolMaximumLocalBadConnectionTolerance 之和。 默認值:3 (新增於 3.4.5)
poolPingQuery – 發送到數據庫的偵測查詢,用來檢驗連接是否正常工作並準備接受請求。默認是“NO PING QUERY SET”,這會導致多數數據庫驅動失敗時帶有一個恰當的錯誤消息。
poolPingEnabled – 是否啓用偵測查詢。若開啓,需要設置 poolPingQuery 屬性爲一個可執行的 SQL 語句(最好是一個速度非常快的 SQL 語句),默認值:false。
poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的頻率。可以被設置爲和數據庫連接超時時間一樣,來避免不必要的偵測,默認值:0(即所有連接每一時刻都被偵測 — 當然僅當 poolPingEnabled 爲 true 時適用)。
PooledDataSource
PooledDataSource
完成池功能的類,直接看拿連接的popConnection
方法:
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
// 觸發獲取連接的當前時間
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null) {
// 同步
synchronized (state) {
// 判斷空閒列表中是否可以提供連接
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// Pool does not have available connection
// 判斷是否達到最大連接數限制
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// Cannot create new connection
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
// 判斷最老一個連接使用時間是否超過最大值
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
// 這裏看到將包裝在oldestActiveConnection中的RealConnection重新用PooledConnection包裝出來直接使用,看前面操作是將連接進行回滾,但是可能失敗,卻不關心,註釋解釋是,在後面的代碼中會進行isValid的判斷,其中就會判斷連接是否可用。
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
// 將老連接設置成invalid
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
// 線程等待,也釋放了鎖
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// ping to server and check the connection is valid or not
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
// 不可用的連接會被設置成null,被回收器回收
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
popConnection
方法實現在一個池中獲取連接的基本邏輯,依賴最大連接數,獲取等待時間,連接使用超時時間等來完成一個池的核心能力。
注意這裏使用wait
方法來等待,在java線程池中使用阻塞隊列來出來暫時拿不到資源的請求。
前面提到,在使用Connection
時,調用close
方法,會調用到dataSource.pushConnection(this);
,就是將這個連接使用完了還回池的動作:
protected void pushConnection(PooledConnection conn) throws SQLException {
// 一樣加鎖
synchronized (state) {
// 從使用線程列表中刪除
state.activeConnections.remove(conn);
if (conn.isValid()) {
// 判斷空閒連接列表是否超過最大值
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
// 加入到空閒連接列表中
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
// 通知等待線程
state.notifyAll();
} else {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
歸還連接時,需要查看空閒列表中的線程數量是否已經到到設置的最大值,如果已經達到,就不需要歸還了,凡是需要加入空閒列表的都需要進行notifyAll
操作,來通知那些等待的線程來搶這個歸還的連接,但是如果此時連接池中空閒連接充足,並沒有線程等待,這個操作也就浪費了,所以可以思考前面popConnection
中的wait
和這裏的notifyAll
是可以用等待隊列來完成。
另外一個方法,用於判斷連接是否可用:
protected boolean pingConnection(PooledConnection conn) {
boolean result = true;
try {
// 先用isClosed來獲取結果
result = !conn.getRealConnection().isClosed();
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
result = false;
}
if (result) {
// 可以通過poolPingEnabled配置來決定是否使用自定義sql
if (poolPingEnabled) {
if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
try {
if (log.isDebugEnabled()) {
log.debug("Testing connection " + conn.getRealHashCode() + " ...");
}
Connection realConn = conn.getRealConnection();
Statement statement = realConn.createStatement();
// 執行poolPingQuery
ResultSet rs = statement.executeQuery(poolPingQuery);
rs.close();
statement.close();
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
result = true;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
}
} catch (Exception e) {
log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
try {
conn.getRealConnection().close();
} catch (Exception e2) {
//ignore
}
result = false;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
}
}
}
}
return result;
}
從代碼中可以看到isClosed
方法並不可靠,最終還是通過執行sql來判斷連接是否可用,這個在很多涉及判斷數據庫連接是否有效的地方都是這麼做的,詳細可以看一下isClosed
方法的註釋。
PooledDataSourceFactory
繼承UnpooledDataSourceFactory,直接返回PooledDataSource對象
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
public PooledDataSourceFactory() {
this.dataSource = new PooledDataSource();
}
}
心得
在整個線程池的實現代碼中,可以學習到一個池的實現的要素有哪些,以及錄用基礎代碼如何實現一個池。對於那些封裝成高層次的池的代碼來說,這個實現顯得又些單薄和不夠全面,可是無論連接池如何實現核心池的實現邏輯是不會變的。