1. 程式人生 > >dbcp原始碼解讀與物件池原理剖析

dbcp原始碼解讀與物件池原理剖析

apache common-pool工具庫是對池化技術原理和具體實現. 

物件池(ObjectPool介面): 可以把它認為是一種容器, 它是用來裝池物件的, 並且包含了用來建立池物件的工廠物件 
池物件:就是要放到池容器中的物件, 理論上可以是任何物件. 
物件池工廠(ObjectPoolFactory介面):用來建立物件池的工廠, 這個沒什麼好說的. 
池物件工廠(PoolableObjectFactory介面):用來建立池物件, 將不用的池物件進行鈍化(passivateObject), 對要使用的池物件進行啟用(activeObject), 對池物件進行驗證(validateObject), 對有問題的池物件進行銷燬(destroyObject)等工作 


物件池中封裝了建立, 獲取, 歸還, 銷燬池物件的職責, 當然這些工作都是通過池物件工廠來實施的, 容器內部還有一個或多個用來盛池物件的容器.物件池會對容器大小, 存放時間, 訪問等待時間, 空閒時間等等進行一些控制, 因為可以根據需要來調整這些設定. 

當需要拿一個池物件的時候, 就從容器中取出一個, 如果容器中沒有的話, 而且又沒有達到容器的最大限制, 那麼就呼叫池物件工廠, 新建一個池物件, 並呼叫工廠的啟用方法, 對建立的物件進行啟用, 驗證等一系列操作. 如果已經達到池容器的最大值, 而物件池中又經沒有空閒的物件, 那麼將會繼續等待, 直到有新的空閒的物件被丟進來, 當然這個等待也是有限度的, 如果超出了這個限度, 物件池就會丟擲異常. 


"出來混, 總是要還的", 池物件也是如此, 當將用完的池物件歸還到物件池中的時候, 物件池會呼叫池物件工廠對該池物件進行驗證, 如果驗證不通過則被認為是有問題的物件, 將會被銷燬, 同樣如果容器已經滿了, 這個歸還池物件將變的"無家可歸", 也會被銷燬, 如果不屬於上面兩種情況, 物件池就會呼叫工廠物件將其鈍化並放入容器中. 在整個過程中, 啟用, 檢查, 鈍化處理都不是必須的, 因此我們在實現PoolableObjectFactory介面的時候, 一般不作處理, 給空實現即可, 所以誕生了BasePoolableObjectFactory. 

當然你也可以將要已有的物件建立好, 然後通過addObject放到物件池中去, 以備後用. 


為了確保對物件池的訪問都是執行緒安全的, 所有對容器的操作都必須放在synchronized中. 

在apache的common-pool工具庫中有5種物件池:GenericObjectPool和GenericKeyedObjectPool, SoftReferenceObjectPool, StackObjectPool, StackKeyedObjectPool. 
五種物件池可分為兩類, 一類是無key的: 

另一類是有key的: 

前面兩種用CursorableLinkedList來做容器, SoftReferenceObjectPool用ArrayList做容器, 一次性建立所有池化物件, 並對容器中的物件進行了軟引用(SoftReference)處理, 從而保證在記憶體充足的時候池物件不會輕易被jvm垃圾回收, 從而具有很強的快取能力. 最後兩種用Stack做容器. 不帶key的物件池是對前面池技術原理的一種簡單實現, 帶key的相對複雜一些, 它會將池物件按照key來進行分類, 具有相同的key被劃分到一組類別中, 因此有多少個key, 就會有多少個容器. 之所以需要帶key的這種物件池, 是因為普通的物件池通過makeObject()方法建立的物件基本上都是一模一樣的, 因為沒法傳遞引數來對池物件進行定製. 因此四種池物件的區別主要體現在內部的容器的區別, Stack遵循"後進先出"的原則並能保證執行緒安全, CursorableLinkedList是一個內部用遊標(cursor)來定位當前元素的雙向連結串列, 是非執行緒安全的, 但是能滿足對容器的併發修改.ArrayList是非執行緒安全的, 便利方便的容器. 

使用物件池的一般步驟:建立一個池物件工廠, 將該工廠注入到物件池中, 當要取池物件, 呼叫borrowObject, 當要歸還池物件時, 呼叫returnObject, 銷燬池物件呼叫clear(), 如果要連池物件工廠也一起銷燬, 則呼叫close(). 
下面是一些時序圖: 
borrowObject: 

returnObject: 

invalidateObject: 


apache的連線池工具庫common-dbcp是common-pool在資料庫訪問方面的一個具體應用.當對common-pool熟悉之後, 對common-dbcp就很好理解了. 它通過對已有的Connection, Statment物件包裝成池物件PoolableConnection, PoolablePreparedStatement. 然後在這些池化的物件中, 持有一個對物件池的引用, 在關閉的時候, 不進行真正的關閉處理, 而是通過呼叫: 

_pool.returnObject(_key)

或: 

_pool.returnObject(_key,this)

這樣一句, 將連線物件放回連線池中. 

而對應的物件池前者採用的是ObjectPool, 後者是KeyedObjectPool, 因為一個數據庫只對應一個連線, 而執行操作的Statement卻根據Sql的不同會分很多種. 因此需要根據sql語句的不同多次進行快取 
在對連線池的管理上, common-dbcp主要採用兩種物件: 
一個是PoolingDriver, 另一個是PoolingDataSource, 二者的區別是PoolingDriver是一個更底層的操作類, 它持有一個連線池對映列表, 一般針對在一個jvm中要連線多個數據庫, 而後者相對簡單一些. 內部只能持有一個連線池, 即一個數據源對應一個連線池. 
下面是common-dbcp的結構關係: 


下面是參考了common-dbcp的例子之後寫的一個從連線池中獲取連線的工具類

/**
 * 建立連線
 *  
 */
public class ConnectionUtils {
	// 一些common-dbcp內部定義的protocol
	private static final String POOL_DRIVER_KEY = "jdbc:apache:commons:dbcp:";
	private static final String POLLING_DRIVER = "org.apache.commons.dbcp.PoolingDriver";


	/**
	 * 取得池化驅動器
	 * 
	 * @return
	 * @throws ClassNotFoundException
	 * @throws SQLException
	 */
	private static PoolingDriver getPoolDriver() throws ClassNotFoundException,
			SQLException {
		Class.forName(POLLING_DRIVER);
		return (PoolingDriver) DriverManager.getDriver(POOL_DRIVER_KEY);
	}


	/**
	 * 銷燬所有連線
	 * 
	 * @throws Exception
	 */
	public static void destory() throws Exception {
		PoolingDriver driver = getPoolDriver();
		String[] names = driver.getPoolNames();
		for (String name : names) {
			driver.getConnectionPool(name).close();
		}
	}


	/**
	 * 從連線池中獲取資料庫連線
	 */
	public static Connection getConnection(TableMetaData table)
			throws Exception {
		String key = table.getConnectionKey();


		PoolingDriver driver = getPoolDriver();


		ObjectPool pool = null;
		// 這裡找不到連線池會拋異常, 需要catch一下
		try {
			pool = driver.getConnectionPool(key);
		} catch (Exception e) {
		}
		
		if (pool == null) {
			// 根據資料庫型別構建連線工廠
			ConnectionFactory connectionFactory = null;
			if (table.getDbAddr() != null
					&& TableMetaData.DB_TYPE_MYSQL == table.getDbType()) {
				Class.forName(TableMetaData.MYSQL_DRIVER);
				connectionFactory = new DriverManagerConnectionFactory(table
						.getDBUrl(), null);
			} else {
				Class.forName(TableMetaData.ORACLE_DRIVER);
				connectionFactory = new DriverManagerConnectionFactory(table
						.getDBUrl(), table.getDbuser(), table.getDbpass());
			}
			
			// 構造連線池
			ObjectPool connectionPool = new GenericObjectPool(null);
			new PoolableConnectionFactory(connectionFactory, connectionPool,
					null, null, false, true);
			
			// 將連線池註冊到driver中
			driver.registerPool(key, connectionPool);
		}


		// 從連線池中拿一個連線
		return DriverManager.getConnection(POOL_DRIVER_KEY + key);
	}
}


Apache Commons Pool實現了物件池的功能。

定義了物件的生成、銷燬、啟用、鈍化等操作及其狀態轉換,並提供幾個預設的物件池實現。

在講述其實現原理前,先提一下其中有幾個重要的物件:

  • PooledObject(池物件)。
  • PooledObjectFactory(池物件工廠)。
  • Object Pool(物件池)。
下面分別詳細講解它們的實現。

PooledObject(池物件)

用於封裝物件(如:執行緒、資料庫連線、TCP連線),將其包裹成可被池管理的物件。提供了兩個預設的池物件實現:
  • DefaultPoolObject。用於非軟引用的普通物件。
  • PooledSoftReference。用於軟引用的物件。
在開發連線池、執行緒池等元件時,需要根據實際情況過載5個方法:startEvictionTest、endEvictionTest、allocate、deallocate和invalidate,用於在不同的場景下修改被包裹物件的內部狀態。 Apache Commons Pool 原始碼分析 | Apache Commons Pool Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代

PooledObject有多種狀態,在不同的環節或經過處理後狀態會發生變化。

狀態 描述
IDLE 位於佇列中,未使用
ALLOCATED 在使用
EVICTION 位於佇列中,當前正在測試,可能會被回收
EVICTION_RETURN_TO_HEAD 不在佇列中,當前正在測試,可能會被回收。從池中借出物件時需要從隊列出移除並進行測試
VALIDATION 位於佇列中,當前正在驗證
VALIDATION_PREALLOCATION 不在佇列中,當前正在驗證。當物件從池中被借出,在配置了testOnBorrow的情況下,對像從佇列移除和進行預分配的時候會進行驗證
VALIDATION_RETURN_TO_HEAD 不在佇列中,正在進行驗證。從池中借出物件時,從佇列移除物件時會先進行測試。返回到佇列頭部的時候應該做一次完整的驗證
INVALID 回收或驗證失敗,將銷燬
ABANDONED 即將無效
RETURN 返還到池中
根據Apache Commons Pool2的預設實現,其狀態變化如下圖所示: Apache Commons Pool2 原始碼分析 | Apache Commons Pool2 Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代

PooledObjectFactory(池物件工廠)

定義了操作PooledObject例項生命週期的一些方法,PooledObjectFactory必須實現執行緒安全。已經有兩個抽象工廠:
  • BasePooledObjectFactory。
  • BaseKeyedPooledObjectFactory。
直接繼承它們實現自己的池物件工廠。 Apache Commons Pool 原始碼分析 | Apache Commons Pool Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代
方法 描述
makeObject 用於生成一個新的ObjectPool例項
activateObject 每一個鈍化(passivated)的ObjectPool例項從池中借出(borrowed)前呼叫
validateObject 可能用於從池中借出物件時,對處於啟用(activated)狀態的ObjectPool例項進行測試確保它是有效的。也有可能在ObjectPool例項返還池中進行鈍化前呼叫進行測試是否有效。它只對處於啟用狀態的例項呼叫
passivateObject 當ObjectPool例項返還池中的時候呼叫
destroyObject 當ObjectPool例項從池中被清理出去丟棄的時候呼叫(是否根據validateObject的測試結果由具體的實現在而定)

Object Pool (物件池)

Object Pool負責管理PooledObject,如:借出物件,返回物件,校驗物件,有多少啟用物件,有多少空閒物件。有三個預設的實現類:
  • GenericObjectPool。
  • ProsiedObjectPool。
  • SoftReferenceObjectPool。
Apache Commons Pool 原始碼分析 | Apache Commons Pool Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代
方法 描述
borrowObject 從池中借出一個物件。要麼呼叫PooledObjectFactory.makeObject方法建立,要麼對一個空閒物件使用PooledObjectFactory.activeObject進行啟用,然後使用PooledObjectFactory.validateObject方法進行驗證後再返回
returnObject 將一個物件返還給池。根據約定:物件必須 是使用borrowObject方法從池中借出的
invalidateObject 廢棄一個物件。根據約定:物件必須 是使用borrowObject方法從池中借出的。通常在物件發生了異常或其他問題時使用此方法廢棄它
addObject 使用工廠建立一個物件,鈍化並且將它放入空閒物件池
getNumberIdle 返回池中空閒的物件數量。有可能是池中可供借出物件的近似值。如果這個資訊無效,返回一個負數
getNumActive 返回從借出的物件數量。如果這個資訊不可用,返回一個負數
clear 清除池中的所有空閒物件,釋放其關聯的資源(可選)。清除空閒物件必須使用PooledObjectFactory.destroyObject方法
close 關閉池並釋放關聯的資源

BorrowObject (借出物件)

下面是GenericObjectPool中borrowObject方法的邏輯實現,有阻塞式和非阻塞式兩種獲取物件的模式。 Apache Commons Pool2 原始碼分析 | Apache Commons Pool2 Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代  

ReturnObject (返還物件)

下面是GenericObjectPool中returnObject方法的邏輯實現,在這裡實現的FIFO(先進先出)和LIFO(後進先出)。 Apache Commons Pool2 原始碼分析 | Apache Commons Pool2 Source Code Analysis - 傲風 - 0與1構築世界,程式設計師創造時代