接上篇文章《LimitLatch 在 Tomcat 中的据源应用》
在Tomcat8之前,tomcat使用的据源默认数据源实现为DBCP,tomcat8之后的据源默认数据源实现为DBCP2。本文基于Tomcat7.0.78(DBCP1.4),据源分析tomcat7数据源的据源源码实现,Tomcat JDBC Connection Pool以及DBCP2的据源实现在后续的文章中进行分析。
首先看一下,据源tomcat文档在宣传Tomcat JDBC Connection Pool时指出的据源DBCP(1.x)的不足:
单线程,为了保证线程安全,据源在获取和归还对象时需要给整个连接池上锁。据源 慢,据源随着CPU数量的据源增长以及获取、归还对象的据源并发线程数的增长,性能堪忧,据源对于高并发系统影响很大。据源 超过60个类,不易维护。 不支持异步获取链接,等等。一、DBCP连接的生命周期
要想读懂DBCP,首先得弄明白一个连接的云服务器生命周期的各个阶段,存在于连接工厂、对象池和连接的使用过程中,简单描述如下:
出生,对象池调用连接工厂的makeObject方法生产一个连接。 校验,通过执行校验SQL,判断当前连接是否可用。 激活,即连接的初始化,设置连接的默认值,如autoCommit等,在获取连接时调用。 借用,调用对象池的borrowObject,从池中获取(或新建)一个对象实例。 使用,应用获得连接后创建Statement,提交事务等。 归还,当调用连接的close方法关闭连接时,实际调用对象池的returnObject方法归还该连接。 钝化,归还连接时调用,云服务器提供商回滚未提交的事务,清除连接的警告,关闭未关闭的资源如Statement等。 销毁,当归还连接时连接已关闭、校验不通过或者发生异常等,则应当销毁该连接而不是归还到连接池中,清理该连接对应的资源,并且关闭物理连接。二、连接池的初始化
当我们通过JNDI拿到数据源并调用其getConnection方法时,实际获取到的数据源实现类是BasicDataSource。BasicDataSource的主要工作就是完成数据源的初始化功能,该工作在***次调用数据源的getConnection方法时完成,一旦完成该部分工作,获取连接的功能实际则交由PoolingDataSource类完成,贴个代码先:
protected synchronized DataSource createDataSource() //同步方法,防止并发请求时创建多个连接池 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.创建连接工厂,用于生产物理连接 // create factory which returns raw physical connections ConnectionFactory driverConnectionFactory = createConnectionFactory(); // 2.创建、源码下载配置连接池,该池即为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. 创建物理连接工厂
根据配置的数据库驱动类名,加载该驱动,并获取Driver实例。此处需要注意的是,首先会在TOMCAT_HOME/lib下加载驱动类,找不到才会使用WebappClassLoader加载,因此如果在tomcat的lib目录和应用的lib目录同时存在数据库驱动,后者是无效的。***,使用获取到的Driver实例和连接的相关属性配置创建了一个连接工厂DriverConnectionFactory的实例并返回,DriverConnectionFactory的作用就是通过Driver实例和属性配置生产物理连接。
2. 生成池
DBCP1.4使用了1.5.4版本的commons-pool来提供对象池功能。根据配置,有GenericObjectPool和AbandonedObjectPool两种实现,AbandonedObjectPool继承了GenericObjectPool,在其基础上添加了跟踪连接泄漏的功能,以下代码为AbandonedObjectPool获取连接时做的工作,可以看到,一个追踪队列加一个获取连接时的事件触发即可实现连接泄漏追踪的功能。
public Object borrowObject() throws Exception { if (config != null && config.getRemoveAbandoned() && (getNumIdle() < 2) && (getNumActive() > getMaxActive() - 3) ) { removeAbandoned();//当可用连接数过少或即将达到***连接数时,遍历追踪队列,看是否存在超时归还的连接 } 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的实现类为PoolableConnectionFactory,在该类中保存了连接池的所有配置以及步骤1中的物理连接工厂等;属性_pool中则存放了实际的所有空闲连接,其实现类CursorableLinkedList为Commons Collections中的实现,是一个双向链表,GenericObjectPool在_pool的头部获取对象,归还连接时根据是否LIFO策略向_pool中的头或者尾添加对象。
3. statement缓存池
statement缓存池使用GenericKeyedObjectPoolFactory实现,其与GenericObjectPool的各个方法的主要思路相同,而区别就是在获取、归还对象等操作时,对应一个key,即一个key一个池,一个Connection对象对应多个statement缓存。
4. 对象池工厂
前面说到GenericObjectPool中需要一个工厂来管理对象的部分生命周期,在这一步生成了PoolableConnectionFactory的实例作为对象池工厂。在准备就绪之后,BasicDataSource还会调用对象池工厂的5个生命周期方法,用以校验整个流程完整无误。
5. 封装
该步骤将前面准备完成的GenericObjectPool池封装为PoolingDataSource,以后的连接获取均通过该PoolingDataSource的getConnection方法返回。连接实际为在前述GenericObjectPool的池中获取,然后封装为PoolGuardConnectionWrapper,该类在调用createStatement、commit等方法时均会检查连接是否已经关闭。同样的,statement在创建时也被封装为了 DelegatingPreparedStatement、DelegatingStatement、DelegatingCallableStatement等,用以检查是否关闭,进行资源回收等。
6. ***进行连接数的初始化,根据配置的最小连接数,生成相应的连接。
三、获取连接
下面重点关注在连接池中获取连接的过程,即Commons Pool中GenericObjectPool的borrowObject方法。
Latch latch = new Latch(); ...... synchronized (this) { ...... _allocationQueue.add(latch); ...... allocate(); }我们看到在获取池中对象时,并没有直接去对应的_pool(存放了空闲对象)中取,而是创建了一个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不为空且池中对象数量没有达到***值,则可创建新的对象。
// 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);//标识可创建新的连接 _numInternalProcessing++; synchronized (latch) { latch.notify(); } } else { break; } }执行到这里,Latch实例存在三种情况:
pair属性中拿到了需要的对象; 没有拿到对象,但mayCreate属性为true,返回后直接创建新的对象; 没有拿到对象,且mayCreate属性为false。如果是情景3,则根据配置的策略,进行异常抛出或者阻塞的处理。阻塞会调用latch的wait方法,等待下次的allocate触发时的notify通知,或者超时失败抛出异常。四、归还连接
限于篇幅原因,后面的功能我们简单看下主要流程,感兴趣的童鞋一定要翻看下源码哦。
当调用连接的close方法时,实际会调用PoolableConnection的close方法。
查看该连接是否已经关闭,如果是,则直接返回。 查看该连接内部的实际物理连接是否已经关闭,如果是,则需要销毁该连接,清理资源(statements),更新监控量。 如果一切正常,则通过连接工厂的passivateObject方法钝化重置后,返回到对象池中。五、语句缓存
前面说到,statement缓存池使用了GenericKeyedObjectPoolFactory实现。在对象池真正创建连接(makeObject)的时候,由PoolableObjectFactory调用底层的DriverConnectionFactory来创建物理连接,然后进行包装。如果配置了使用语句缓存,则中间会多包装一层PoolingConnection。PoolingConnection重载了prepareStatement等方法,负责在创建语句时首先到statement缓存池获取。可以看到,DBCP的语句缓存是通过层层包装(装饰模式)来实现的。
六、总结一下
DBCP1.X是一个古老的数据源实现,1.2版本甚至可以追溯到10年之前,但时至今日,笔者仍能在众多项目(主要是Spring托管)中看到他的身影,虽然一方面的原因是项目缺乏开拓性,这也从侧面证实了DBCP确实能够满足大多数项目的需求。在后面的数据源系列文章中我们将继续分析Tomcat中其的他数据源实现,并进行性能测试。
【本文为专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】
戳这里,看该作者更多好文