Tomcat 的數(shù)據(jù)源(一)
接上篇文章《LimitLatch 在 Tomcat 中的應用》
在Tomcat8之前,tomcat使用的默認數(shù)據(jù)源實現(xiàn)為DBCP,tomcat8之后的默認數(shù)據(jù)源實現(xiàn)為DBCP2。本文基于Tomcat7.0.78(DBCP1.4),分析tomcat7數(shù)據(jù)源的源碼實現(xiàn),Tomcat JDBC Connection Pool以及DBCP2的實現(xiàn)在后續(xù)的文章中進行分析。
首先看一下,tomcat文檔在宣傳Tomcat JDBC Connection Pool時指出的DBCP(1.x)的不足:
- 單線程,為了保證線程安全,在獲取和歸還對象時需要給整個連接池上鎖。
- 慢,隨著CPU數(shù)量的增長以及獲取、歸還對象的并發(fā)線程數(shù)的增長,性能堪憂,對于高并發(fā)系統(tǒng)影響很大。
- 超過60個類,不易維護。
- 不支持異步獲取鏈接,等等。
一、DBCP連接的生命周期
要想讀懂DBCP,首先得弄明白一個連接的生命周期的各個階段,存在于連接工廠、對象池和連接的使用過程中,簡單描述如下:
- 出生,對象池調用連接工廠的makeObject方法生產一個連接。
- 校驗,通過執(zhí)行校驗SQL,判斷當前連接是否可用。
- 激活,即連接的初始化,設置連接的默認值,如autoCommit等,在獲取連接時調用。
- 借用,調用對象池的borrowObject,從池中獲取(或新建)一個對象實例。
- 使用,應用獲得連接后創(chuàng)建Statement,提交事務等。
- 歸還,當調用連接的close方法關閉連接時,實際調用對象池的returnObject方法歸還該連接。
- 鈍化,歸還連接時調用,回滾未提交的事務,清除連接的警告,關閉未關閉的資源如Statement等。
- 銷毀,當歸還連接時連接已關閉、校驗不通過或者發(fā)生異常等,則應當銷毀該連接而不是歸還到連接池中,清理該連接對應的資源,并且關閉物理連接。
二、連接池的初始化
當我們通過JNDI拿到數(shù)據(jù)源并調用其getConnection方法時,實際獲取到的數(shù)據(jù)源實現(xiàn)類是BasicDataSource。BasicDataSource的主要工作就是完成數(shù)據(jù)源的初始化功能,該工作在***次調用數(shù)據(jù)源的getConnection方法時完成,一旦完成該部分工作,獲取連接的功能實際則交由PoolingDataSource類完成,貼個代碼先:
- protected synchronized DataSource createDataSource() //同步方法,防止并發(fā)請求時創(chuàng)建多個連接池
- throws SQLException {
- if (closed) {
- throw new SQLException("Data source is closed");
- }
- // 如果連接池已經被初始化,直接返回PoolingDataSource
- // Return the pool if we have already created it
- if (dataSource != null) {
- return (dataSource);
- }
- // 1.創(chuàng)建連接工廠,用于生產物理連接
- // create factory which returns raw physical connections
- ConnectionFactory driverConnectionFactory = createConnectionFactory();
- // 2.創(chuàng)建、配置連接池,該池即為GenericObjectPool對象
- // create a pool for our connections
- createConnectionPool();
- // 3.statement緩存池
- // Set up statement pool, if desired
- GenericKeyedObjectPoolFactory statementPoolFactory = null;
- if (isPoolPreparedStatements()) {
- statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
- -1, // unlimited maxActive (per key)
- GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
- 0, // maxWait
- 1, // maxIdle (per key)
- maxOpenPreparedStatements);
- }
- //4.又一個連接工廠,生產的是物理連接的包裝對象,供GenericObjectPool調用
- // Set up the poolable connection factory
- createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig);
- // 5.封裝
- // Create and return the pooling data source to manage the connections
- createDataSourceInstance();
- // 6.連接初始化
- try {
- for (int i = 0 ; i < initialSize ; i++) {
- connectionPool.addObject();
- }
- } catch (Exception e) {
- throw new SQLNestedException("Error preloading the connection pool", e);
- }
- return dataSource;
- }
1. 創(chuàng)建物理連接工廠
根據(jù)配置的數(shù)據(jù)庫驅動類名,加載該驅動,并獲取Driver實例。此處需要注意的是,首先會在TOMCAT_HOME/lib下加載驅動類,找不到才會使用WebappClassLoader加載,因此如果在tomcat的lib目錄和應用的lib目錄同時存在數(shù)據(jù)庫驅動,后者是無效的。***,使用獲取到的Driver實例和連接的相關屬性配置創(chuàng)建了一個連接工廠DriverConnectionFactory的實例并返回,DriverConnectionFactory的作用就是通過Driver實例和屬性配置生產物理連接。
2. 生成池
DBCP1.4使用了1.5.4版本的commons-pool來提供對象池功能。根據(jù)配置,有GenericObjectPool和AbandonedObjectPool兩種實現(xiàn),AbandonedObjectPool繼承了GenericObjectPool,在其基礎上添加了跟蹤連接泄漏的功能,以下代碼為AbandonedObjectPool獲取連接時做的工作,可以看到,一個追蹤隊列加一個獲取連接時的事件觸發(fā)即可實現(xiàn)連接泄漏追蹤的功能。
- public Object borrowObject() throws Exception {
- if (config != null
- && config.getRemoveAbandoned()
- && (getNumIdle() < 2)
- && (getNumActive() > getMaxActive() - 3) ) {
- removeAbandoned();//當可用連接數(shù)過少或即將達到***連接數(shù)時,遍歷追蹤隊列,看是否存在超時歸還的連接
- }
- Object obj = super.borrowObject();//從父類即GenericObjectPool獲取連接
- if (obj instanceof AbandonedTrace) {
- ((AbandonedTrace) obj).setStackTrace();//記錄堆棧,方便排查問題
- }
- if (obj != null && config != null && config.getRemoveAbandoned()) {
- synchronized (trace) {
- trace.add(obj);//獲取連接成功,添加到追蹤隊列
- }
- }
- return obj;
- }
GenericObjectPool中有兩個重要的屬性:_factory和_pool。屬性_factory為接口PoolableObjectFactory的實例,管理了對象生命周期中的五個階段:生產、銷毀、激活、鈍化、校驗,DBCP中PoolableObjectFactory的實現(xiàn)類為PoolableConnectionFactory,在該類中保存了連接池的所有配置以及步驟1中的物理連接工廠等;屬性_pool中則存放了實際的所有空閑連接,其實現(xiàn)類CursorableLinkedList為Commons Collections中的實現(xiàn),是一個雙向鏈表,GenericObjectPool在_pool的頭部獲取對象,歸還連接時根據(jù)是否LIFO策略向_pool中的頭或者尾添加對象。
3. statement緩存池
statement緩存池使用GenericKeyedObjectPoolFactory實現(xiàn),其與GenericObjectPool的各個方法的主要思路相同,而區(qū)別就是在獲取、歸還對象等操作時,對應一個key,即一個key一個池,一個Connection對象對應多個statement緩存。
4. 對象池工廠
前面說到GenericObjectPool中需要一個工廠來管理對象的部分生命周期,在這一步生成了PoolableConnectionFactory的實例作為對象池工廠。在準備就緒之后,BasicDataSource還會調用對象池工廠的5個生命周期方法,用以校驗整個流程完整無誤。
5. 封裝
該步驟將前面準備完成的GenericObjectPool池封裝為PoolingDataSource,以后的連接獲取均通過該PoolingDataSource的getConnection方法返回。連接實際為在前述GenericObjectPool的池中獲取,然后封裝為PoolGuardConnectionWrapper,該類在調用createStatement、commit等方法時均會檢查連接是否已經關閉。同樣的,statement在創(chuàng)建時也被封裝為了 DelegatingPreparedStatement、DelegatingStatement、DelegatingCallableStatement等,用以檢查是否關閉,進行資源回收等。
6. ***進行連接數(shù)的初始化,根據(jù)配置的最小連接數(shù),生成相應的連接。
三、獲取連接
下面重點關注在連接池中獲取連接的過程,即Commons Pool中GenericObjectPool的borrowObject方法。
- Latch latch = new Latch();
- ......
- synchronized (this) {
- ......
- _allocationQueue.add(latch);
- ......
- allocate();
- }
我們看到在獲取池中對象時,并沒有直接去對應的_pool(存放了空閑對象)中取,而是創(chuàng)建了一個Latch對象,然后將該對象放入一個LinkedList中,然后調用allocate方法。LinkedList中的每一個Latch都代表了一個待獲取連接的線程。
allocate是一個同步方法,做了兩部分工作:
1. 如果有空閑對象且等待獲取對象的_allocationQueue不為空,中和兩者。
- // First use any objects in the pool to clear the queue
- for (;;) {
- if (!_pool.isEmpty() && !_allocationQueue.isEmpty()) {
- Latch latch = (Latch) _allocationQueue.removeFirst();//取出***個等待線程
- latch.setPair((ObjectTimestampPair) _pool.removeFirst());//將池中空閑連接分配至線程
- _numInternalProcessing++;
- synchronized (latch) {
- latch.notify();//通知等待該連接的線程
- }
- } else {
- break;
- }
- }
2. 如果仍有等待獲取對象的_allocationQueue不為空且池中對象數(shù)量沒有達到***值,則可創(chuàng)建新的對象。
- // Second utilise any spare capacity to create new objects
- for(;;) {
- if((!_allocationQueue.isEmpty()) && (_maxActive < 0 || (_numActive + _numInternalProcessing) < _maxActive)) {
- Latch latch = (Latch) _allocationQueue.removeFirst();
- latch.setMayCreate(true);//標識可創(chuàng)建新的連接
- _numInternalProcessing++;
- synchronized (latch) {
- latch.notify();
- }
- } else {
- break;
- }
- }
執(zhí)行到這里,Latch實例存在三種情況:
- pair屬性中拿到了需要的對象;
- 沒有拿到對象,但mayCreate屬性為true,返回后直接創(chuàng)建新的對象;
- 沒有拿到對象,且mayCreate屬性為false。如果是情景3,則根據(jù)配置的策略,進行異常拋出或者阻塞的處理。阻塞會調用latch的wait方法,等待下次的allocate觸發(fā)時的notify通知,或者超時失敗拋出異常。
四、歸還連接
限于篇幅原因,后面的功能我們簡單看下主要流程,感興趣的童鞋一定要翻看下源碼哦。
當調用連接的close方法時,實際會調用PoolableConnection的close方法。
- 查看該連接是否已經關閉,如果是,則直接返回。
- 查看該連接內部的實際物理連接是否已經關閉,如果是,則需要銷毀該連接,清理資源(statements),更新監(jiān)控量。
- 如果一切正常,則通過連接工廠的passivateObject方法鈍化重置后,返回到對象池中。
五、語句緩存
前面說到,statement緩存池使用了GenericKeyedObjectPoolFactory實現(xiàn)。在對象池真正創(chuàng)建連接(makeObject)的時候,由PoolableObjectFactory調用底層的DriverConnectionFactory來創(chuàng)建物理連接,然后進行包裝。如果配置了使用語句緩存,則中間會多包裝一層PoolingConnection。PoolingConnection重載了prepareStatement等方法,負責在創(chuàng)建語句時首先到statement緩存池獲取。可以看到,DBCP的語句緩存是通過層層包裝(裝飾模式)來實現(xiàn)的。
六、總結一下
DBCP1.X是一個古老的數(shù)據(jù)源實現(xiàn),1.2版本甚至可以追溯到10年之前,但時至今日,筆者仍能在眾多項目(主要是Spring托管)中看到他的身影,雖然一方面的原因是項目缺乏開拓性,這也從側面證實了DBCP確實能夠滿足大多數(shù)項目的需求。在后面的數(shù)據(jù)源系列文章中我們將繼續(xù)分析Tomcat中其的他數(shù)據(jù)源實現(xiàn),并進行性能測試。
【本文為51CTO專欄作者“侯樹成”的原創(chuàng)稿件,轉載請通過作者微信公眾號『Tomcat那些事兒』獲取授權】