Apache Tomcat 7系列的版本中使用了JDBC Connection Pool替代传统的commons-dbcp作为新的数据库连接池。其中很重要的一个原因是,commons-dbcp是单线程的,为了保证线程安全它必须将整个线程池上锁,并且它在对高并发的支持方面表现很差。JDBC Connection Pool一个很重要的新特性就是它对高并发环境和多核/多处理器系统的支持。下面将通过对JDBC Connection Pool的源码分析,深入理解其连接池设计的思想以及对高并发的解决方法。

连接池的存储设计

连接池使用两个阻塞队列BlockingQueue分别存储已分配的active和空闲的idle连接对象。如果BlockingQueue是空的,从BlockingQueue中取元素会被阻断进入等待状态,直到BlockingQueue进入元素后才被唤醒;同样,如果BlockingQueue是满的,任何试图往队列里存元素也会被阻断进入等待状态,直到BlockingQueue中有空间时才会被唤醒继续操作。
Tomcat 7 JDBC Connection Pool中的FairBlockingQueue实现了BlockingQueue接口。FairBlockingQueue采用先进先出(FIFO)原理,从而保证最先进入idle队列的连接最先被分配。FairBlockingQueue的offer和poll操作时均使用ReentrantLock锁定该连接,操作成功后解锁。ReentrantLock锁是一个可重入的互斥锁,它由最近成功获得锁定,并且还没有释放该锁的线程拥有。所以FairBlockingQueue在offer和poll一个连接对象时,能够确保该连接不会被其它并发的请求干扰,导致死锁。(后面小节中提到的锁均是指ReentrantLock)。

并发请求与连接对象的映射

当同时有多个用户请求连接对象时,若有足够的连接对象,则每个用户根据请求的时间依次从idle队列中获取一个连接对象。若idle队列中没有足够的队列,则需要按请求时间依次将这些用户请求添加到一个先进先出FIFO的受阻塞的请求队列,每次添加都是在队列尾部插入,从而保证一旦有空闲连接产生时,先来的请求最先获得连接。一旦请求获得连接对象后,它将从受阻塞的请求队列中移除。在向受阻塞的请求队列中添加和移除请求时,都需要对队列上锁,防止同时有多个请求被添加、或同时有多个请求被移除、或同时既有请求被添加又有请求被移除等情况发生,导致请求队列的乱序,直到操作完成后解锁。

连接池为每一个没有分配到连接的请求设置一个同步倒数计数器CountDownLatch,每个计数器传入的初始值分别为请求到来的顺序。例如,最早来到的且未被分配连接的请求的同步计数器初值设为1,后面来到的请求初值依次加1。计数器大于0,该线程就会被阻塞,直到计数器为0,该线程才能继续执行。每次当有空闲连接产生,并且成功分配给受阻塞的请求队列最前面的请求后,所有被阻塞的请求的计数器减1,因此刚获取连接对象的请求线程的计数器值变为0,从而可以继续执行,其它请求线程则继续处于阻塞状态。

在Tomcat 7 JDBC Connection Pool中,自定义了ExchangeCountDownLatch类,该类继承CountDownLatch,并且添加了一个成员变量,表示该请求线程最终被分配到的连接对象。

下面通过一个例子说明以上过程:如下图1中Time=1时,受阻塞请求队列中有n个请求线程受阻塞,各自的同步计数器值分别从1到n,此时空闲连接队列为空,工作连接队列中队尾的连接为Cm;假设Time=1结束后,工作连接队列中的C1被释放,则在Time=2时,受阻塞请求队列的队首R1(count=1)线程获得刚被释放的C1连接,因此它被移除受阻塞请求队列,队首请求变为R2,并且所有的受阻塞的请求线程计数器都减1,此时R1的count=0可以继续执行,R2的count=1,继续被阻塞。C1连接因为被R1所占用,因此又被添加到工作队列的队尾。

获取连接对象的流程

当用户请求连接对象时,若连接池中有空闲连接则立即返回该连接,若没有空闲连接但是连接总数没有达到设定的最大值,则创建一个新的连接对象并返回。否则循环等待并检查是否有空闲连接,若有空闲连接产生则返回该连接,若超过等待时间仍无空闲连接产生,则抛出异常。下图2详细说明了请求连接对象的流程。


在检查到idle队列中存在空闲连接后还需要对连接作有效性的检查,以确定该连接对象与数据库是处于连接状态,若连接断开需重新连接,以保证该连接对象的有效性。为了确保多个请求线程不会竞争同一个连接对象,首先获得该连接的线程需要对该连接上锁,直到检查完毕确保该连接有效并被添加到active队列后才为其解锁。