簡單實現
說到數據庫連接池的實現,可能大家並不陌生,應該都或多或少的實現過,那麼先來說說一個簡單的數據庫連接池的實現。
既然是連接池,首先得有連接,然後有池子(廢話),連接使用jdk的Connection,池子使用一個List<Connection>
即可,需要連接的時候從list中獲取,如果list中沒有那麼就新new一個並加入到list中就可以了。使用完成之後,將連接放回list中即可,這樣一個最簡單的連接池就實現了。(PS:相關接口一定要是線程安全的)
上面實現了一個很簡單的線程池,滿足了大部分的使用場景,但是從程序的性能、併發性、可擴展性、可維護性、易用性等方面還有一定的欠缺,下面我們將列舉出上面簡單的連接池的一些不足之處,針對這些不足之處一步一步進行改進。
- 設置最大連接數量
不能無節制的創建新的連接,應該對最大的連接數量進行限制,如果超過了限制的連接數量,或者是無法從老的連接中恢復一個新的連接,那麼就拋出一定一場給調用者,調用者可以根據異常來決定將請求緩存起來等到有新的可用的連接的時候再處理。 - 釋放空閒連接
如果某一個時刻併發度比較高,那麼連接池中的連接的數量將等於程序啓動以來最大的併發數,但是在併發比較小的時候,[1]連接池中的連接沒有被使用,但是卻一直佔用着內存空間,是一種資源的浪費;[2]即使目前併發度非常的小,但是也需要保留核心數量的連接,應對突然爆發的併發,總結起來就是需要設置poolMaximumIdleConnections(最大空閒的連接數量), - 設置獲取連接的超時
設置連接獲取超時等值,這樣可以防止無止境的等待。
……..
爲了獲取更好的性能,需要優化的東西還很多,幸好目前在業界也有很多相應的開源的實現,可以通過分析優秀的開源實現來深入理解如果設計好一個優秀的連接池。
mybatis的連接池的實現
上面闡述了一個簡單的連接池的實現,下面將對mybatis的數據庫連接池的實現做一個源碼級的分析,只要深入到了源碼才能正確時的調用API。
其實mybatis中的連接池的實現和上面的簡單連接池的實現時一樣的,只是把上面討論的那些需要優化的點實現了,在mybatis中數據庫連接池的實現主要在類PooledDataSource中,直接找到該類的getConnection方法可以看到,該方法調用了一個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) {
// 1、首先從空閒連接池獲取連接
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
// 2、然後判斷連接池的連接數量是否達到上限
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");
}
}
// 回滾成功,將該連接分配給此次連接請求
// 下面使用了連接代理的方式創建,這種方式可以攔截Connection的close方法
// 實現連接的回收(後續會詳細講解具體的實現)
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
// 通過上面的方式無法獲取到新的連接,阻塞等待獲取新的連接,如果在等待過程中
// 出現了中斷異常,那麼就會退出該次循環,那麼就可能導致conn=null
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
// 上面的操作不一定能保證此次請求完全能獲取一個可用的連接
// 所以需要再次判斷conn是否可用,如果該連接上仍然有任務,那麼回滾在連接上的任務;
// 如果該連接上沒有任務,那麼直接返回。
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++;
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;
}
從上面的分析,我們知道了該方法API要麼返回正常的數據庫連接,要麼拋出異常信息,調用者需要捕獲一場信息,並及時的緩存此次數據庫操作請求。
上面說到了返回Connection的時候進行了代理的封裝,該代理是PooledConnection,該類是jdk的Connection一個代理,下面主要通過查看該代理的invoke方法來說一說該代理類的主要作用
private static final String CLOSE = "close";
private final Connection realConnection;
......
/*
* 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();
// 判斷是否是close方法
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// 如果是close方法,那麼只是將該連接重新放回連接池中,並不真正的關閉連接
dataSource.pushConnection(this);
return null;
} else {
// 如果不是close方法,那麼通過反射的方式調用真正的Connection執行
// (需要注意的是PooledConnection只是一個代理,正在執行的還是jdk的Connection)
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);
}
}
}
這種設計方式可以使用任何連接建立消耗大的場景,如在客戶端的socket連接池的使用,如jsch的ssh的會話連接池。
學習框架源碼也是有套路的!