tomcat-jdbc Pool 原始碼實現簡單分析
=================================
什麼是連線池?
池,不由自主的會想到水池。
小時候,我們都要去遠處的水井挑水,倒進家中的水池裡面。這樣,每次要用水時,直接從水池中「取」就行了。不用大老遠跑去水井打水。
資料庫連線池就如此,我們預先準備好一些連線,放到池中。當需要時,就直接獲取。而不要每次跟資料庫建立一個新的連線。特別對資料庫連線這類耗時,耗資源的操作。當連線用完後,再放回池中,供後續使用。
連線池的作用?
避免多次去建立資源。
例如,建立新的資料庫連線,500ms輕輕鬆鬆就消耗了。建立TCP連線,資料庫賬號驗證等等。這效能消耗起來,可是非常大的。
在稍大的系統內,連線池是必備的。同時,對技術人員要求,對連線池的掌握也是必須的。
tomcat-jdbc-pool的特色
基於jdk1.5
後的併發實現。程式碼簡潔,精練。核心的類就2,3個。
對池的控制就在 org.apache.tomcat.jdbc.pool.ConnectionPool
中搞定。
先前有簡單看過 dbcp1.x, c3p0等等,程式碼量真不少,邏輯複雜。
想熟悉池的設計,可以仔細讀讀tomcat-jdbc-pool,非常快速的入手。在dbcp2的實現時,跟tomcat-jdbc-pool思路一致(完全copy的版本)
對於連線池來說,最基本的特點就是:
- 有一定的容量,及已經建立好的物件
- 有「借」有「還」操作的介面
池中「借出」連線是怎麼個過程?
在jdbc-pool
設計有2佇列,分別為busy
和idle
,儲存「正在使用」和「空閒」的連線。都採用ArrayBlockingQueue
以保證執行緒安全。
當有請求「借」的動作過來時,從idle
中poll
一個連線,然後將該連線再offer
至busy
佇列中。這是最基本最純淨的思路。
當idle
連線不夠時,內部會再去建立新的連線返回給客戶端。
但是,做為「池」必須的職責之一是控制總量,不會任你去增長。
那麼,有意思來了,他是怎麼控制總量的咧?
我們可以通俗點稱『佔坑法』(tomcat中也有不少場景採用這方式)。
首先池中有維護連線數總量「計數器」(採用AtomicInteger
『佔坑法』就在每次要新建立連線池,先總量計數器+1
(佔位),再比較是否達到配置的池的最大連線數。如果沒有達到,則建立新的;如果已達到了,則等待現有連線釋放,再取走。
有點類似,大學時先用本書去搶位置佔著。
大致實現程式碼【已經對原始碼簡化】如下:
public class ConnectionPool {
//連線數的總量
private AtomicInteger size = new AtomicInteger(0);
//所有正在使用中的連線
private BlockingQueue<PooledConnection> busy;
//所有空閒的連線
private BlockingQueue<PooledConnection> idle;
PooledConnection con = idle.poll();//
while (true) {
if (con != null) {//如果從空閒連線佇列中取出的連線不為空
//把這個連線加入正在使用中的連線列表,***並返回
busy.offer(con);//這簡化了,在原始碼中這兒會對連線進行校驗、檢查或進行連線。
return con;
}
//下面判斷空閒連線佇列中取出的連線為空的情況
//沒有超出連線總數
if (size.get() < getPoolProperties().getMaxActive()) {
//佔坑神技
if (size.addAndGet(1) > getPoolProperties().getMaxActive()) {//超出連線總數
//既然沒了,那數量也減回去
//再去等待其他連線歸還回來
size.decrementAndGet();
} else {
//新建一個物理連線
return createConnection(now, con, username, password);
}
}
//等待一定時間timetowait後,再次從空閒連線佇列中取出的連線,迴圈執行上面過程
con = idle.poll(timetowait, TimeUnit.MILLISECONDS);
}
}
}
用完後「歸還」連線是怎麼個過程?
大致思路跟「借」操作相反落。當然是無視那些「善後」的工作,只關注資源的管理。
但是,做為連線池必須的職責之一,並不真實的斷開與資料庫的連線。而只是放至idle
佇列中,供客戶端下次再使用。如果有需要或必要肯定會釋放,技巧所在。
大致程式碼如下:
protected void returnConnection(PooledConnection con) {
if (con != null) {
try {
con.lock();
if (busy.remove(con)) {
//跟允許的最大空閒數比較
if(idle.size() < poolProperties.getMaxIdle()) {
idle.offer(con);
//原始碼中呼叫release
//會根據配置項執行一些校驗,例如:testOnReturn為true,則在回收時檢查連線是否正常
//release(con);
}
} catch(Exception e) {
//....
} finally {
con.unlock();
}
} //end if
}
當長時間執行後,怎麼回收無效的連線?
這是連線池必備的功能之一,類似檢查死鏈或者釋放自身過多的資源。比如,在高併發過後,對資源消耗量少時,就釋放些不再使用的資料庫連線(真實斷開),維護合理的空格數量。【*** 連線池的minIdle定義了這個數量】
看到這應用場景就自然想到,通過後臺執行緒定時掃描。
「對的,就是這樣子。」
同樣在ConnectionPool
這個類檔案中的PoolCleaner
類。寫在同個類檔案中,便於用this進行傳遞資料。不用再去構造個複雜的ConnectionPool
物件。
直接上程式碼,「好程式碼」就是最好的描述。
public class ConnectionPool {
/**
* Initialize the connection pool - called from the constructor
*/
protected void init(PoolConfiguration properties) throws SQLException {
initializePoolCleaner(properties);
}
public void initializePoolCleaner(PoolConfiguration properties) {
if (properties.isPoolSweeperEnabled()) {
poolCleaner = new PoolCleaner(this, properties.getTimeBetweenEvictionRunsMillis());
poolCleaner.start(); //只註冊一個清理器,並未啟動執行緒。
} //end if
}
/**
* 檢查所有的空閒連線
*/
public void checkIdle(boolean ignoreMinSize) {
try {
if (idle.size()==0) return;
Iterator<PooledConnection> unlocked = idle.iterator();
//【重點:如果空閒數量大於屬性MinIdle,則執行清理!】
while ( (idle.size()>=getPoolProperties().getMinIdle())) &&unlocked.hasNext()) {
PooledConnection con = unlocked.next();
try {
con.lock();
//如果這時已到busy中,則不檢查了
if (busy.contains(con)) {
continue;
}
release(con);//釋放了物理連線
idle.remove(con);//從空閒佇列中移除
} finally {
con.unlock();
}
} //while
} catch (Exception e) {
//....
}
}
private static volatile Timer poolCleanTimer = null;
private static HashSet<PoolCleaner> cleaners = new HashSet<>();
//註冊一個清理器
private static synchronized void registerCleaner(PoolCleaner cleaner) {
unregisterCleaner(cleaner);
cleaners.add(cleaner);
//一堆構造方式。。。
if (poolCleanTimer == null) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader());
poolCleanTimer = new Timer("PoolCleaner["+ System.identityHashCode(ConnectionPool.class.getClassLoader()) + ":"+
System.currentTimeMillis() + "]", true);
}finally {
Thread.currentThread().setContextClassLoader(loader);
}
}
//構造定時掃描器
//java有內庫非常強大,想用啥有啥呀
poolCleanTimer.scheduleAtFixedRate(cleaner, cleaner.sleepTime,cleaner.sleepTime);
}
//真實的處理執行緒在這兒。。。
protected static class PoolCleaner extends TimerTask {
protected WeakReference<ConnectionPool> pool;
PoolCleaner(ConnectionPool pool, long sleepTime) {
//弱引用,不瞭解的可以google下
this.pool = new WeakReference<>(pool);
}
@Override
public void run() {
ConnectionPool pool = this.pool.get();
if (pool == null) {
stopRunning();
} else if (!pool.isClosed()) {
if (pool.getPoolProperties().getMinIdle() < pool.idle.size()) {
pool.checkIdle(); //【重點,如果空閒數量大於MinIdle,則執行checkIdle】
}
}
}
public void start() {
registerCleaner(this); //並未啟動執行緒,只是註冊一個清理器
}
public void stopRunning() {
unregisterCleaner(this);
}
}
}