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