数据库连接池BoneCP源码分析报告

不错,弄上来一起学习。

数据库连接池BONECP源码分析报告 1
1. 简述 2
1.1  官方主页 2
1.2  API文档 2
1.3  BoneCP简介(译自官方) 2
1.4  BoneCP特点(译自官方) 2
1.5  本次分析使用的版本 3
1.6  依赖库 3
1.7  包结构说明 3
1.8  主要类型 3
1.9  连接池创建过程及连接获取过程简述 4
2.  BONECP生命周期过程 4
2.1  BoneCPConfig初始化及配置分析 4
2.2  BoneCPDataSource分析 5
2.3  BoneCP初始化过程分析 5
2.4  BoneCP的getConnection()过程分析 6
2.5  BoneCP的shutdown 6
3.  BONECP使用的队列数据结构分析 7
3.1  LIFOQueue 7
3.2  LinkedBlockingDeque(JDK1.6) 7
3.3  BoundedLinkedTransferQueue 8
3.4  LinkedTransferQueue(将纳入JDK1.7) 8
4.BONECP线程池 10
5.发现BONECP-0.7.0中的BUG 12













数据库连接池BoneCP源码分析报告

作者:葛一帆 时间:2011-03-13


1. 简述
1.1  官方主页
http://jolbox.com/

1.2  API文档
http://jolbox.com/bonecp/downloads/site/apidocs/index.html

1.3  BoneCP简介(译自官方)
BoneCP是一个快速,免费,开源的Java数据库连接池(即,JDBC Pool)。如果熟悉C3P0或者DBCP,那么你也就知道它是用来干什么的。简单地说,这个代码库将为你管理数据库连接,让你的应用具有更快的数据库访问能力。

1.4  BoneCP特点(译自官方)
 具有高可扩展性的快速连接池
 在connection状态改变时,可配置回调机制(钩式拦截器)
 通过分区(Partitioning)来提升性能
 允许你直接访问connection或statement
 自动地扩展pool容量
 支持statement caching
 支持异步地获取connection(通过返回一个Future<Connection>)
 以异步的方式,施放辅助线程(helper threads)来关闭connection和statement,以获得高性能。
 在每个新获取的connection上,通过简单的机制,执行自定义的statement,(即通过简单的SQL语句来测试connection是否有效,对应的配置属性为initSQL)
 支持运行时切换数据库,而不需要停止(shut down)应用
 能够自动地回放(replay)任何失败的事务(例如,数据库或网络出现故障等等)
 支持JMX
 可以延迟初始化(lazy initialization)
 支持使用XML或property文件的配置方式
 支持idle connection timeouts和max connection age
 自动检验connection(是否活跃等等)
 允许直接从数据库获取连接,而不通过Driver
 支持Datasouce和Hibernate
 支持通过debugging hooks来定位获取后未关闭的connection
 支持通过debugging来显示被关闭了两次的connection的堆栈轨迹(stack locations)
 支持自定义pool name
 整洁有序的代码
 免费,开源,纯Java编写,具有完整的文档。


1.5  本次分析使用的版本
bonecp-0.7.0.jar

1.6  依赖库
slf4j-api-1.5.8.jar
slf4j-nop-1.5.8.jar
guava-r08.jar (google lib)

1.7  包结构说明

主要包
com.jolbox.bonecp 连接池核心包
com.jolbox.bonecp.hooks 支持connection状态改变时的事件通知
com.jolbox.bonecp.proxy 代理java.sql.*下的接口,仅供内部使用
jsr166y 将jsr166y的部分类型直接打包进来

其它包,提供外部支持和测试
com.jolbox.bonecp.provider
com.jolbox.bonecp.spring
com.jolbox.benchmark


1.8  主要类型
com.jolbox.bonecp.BoneCP
com.jolbox.bonecp.BoneCPConfig
com.jolbox.bonecp.BoneCPDataSource
com.jolbox.bonecp包下的多种线程及包装类

1.9  连接池创建过程及连接获取过程简述
代码:
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//实例化配置类
BoneCPConfig config = new BoneCPConfig();
//设置配置
config.setJdbcUrl("jdbc:mysql://localhost:3306/sailing_db");
config.setUsername("root");
config.setPassword("root");
config.setXxx…
//创建连接池
BoneCP pool = new BoneCP(config);
//从池中获取连接
Connection connection = pool.getConnection();
System.out.println(connection.getClass().getName());
System.out.println("--->"+connection.getAutoCommit());
connection.close();
pool.shutdown();
注:以上是使用BoneCP的基本方式,其它方式,比如数据源支持,读取配置文件等,都是对该过程的进一步封装,以“适配”不同的使用需求。下面开始进行源码分析,对该连接池的运行过程进行详细的阐述。

2.  BoneCP生命周期过程

2.1  BoneCPConfig初始化及配置分析
BoneCPConfig提供了以XML和Properties两种配置方式。它的无参构造方法将会加载类路径下的XML配置文件,然后调用JDK的XML解析库对XML进行解析(即javax.xml..下)。XML文件中的配置信息最终会被转化成java.util.Properties对象,然后调用设置setProperties的方法进行属性设置。setProperties方法接收Properties对象作为参数,并借助Java反射机制,用一次for循环将属性注入到字段中。BoneCPConfig将属性字段的类型限定为4种,分别为:int, long, boolean和String。
除了常规的连接池设置,配置属性中需要特别说明字段如下:
connectionHook
initSQL

2.2  BoneCPDataSource分析
BoneCP提供的数据源BoneCPDataSource继承了BoneCPConfig,它实现了DataSource接口,还实现了ObjectFactory接口,支持JNDI的支持。该数据源持有一个BoneCP对象(即pool字段),该字段使用了volatile关键字进行修饰,
private transient volatile BoneCP pool;
这里使用volatile是没有必要的,应该做成final的,并且不使用Lazy加载,因为每个数据源对应一个唯一的pool,只要使用相同的数据源对象,这个pool句柄就不会改变。即使在多线程环境下,只要使用的数据源对象没有改变,那么使用的就是相同的pool对象。唯一的风险在于初期调用getConnection时存在竞争。

2.3  BoneCP初始化过程分析
执行new BoneCP(config),在构造方法中,将主要进行以下操作:
1. 调用config.sanitize()方法,纠正config中的错误配置。例如,如果属性 maxConnectionsPerPartition(每个分区的最大)小于2,那么就将该变量设置为50。
2. 根据config中的属性来设置BoneCP的一些常规属性。
3. 如果不进行connection的延迟测试(即config.isLazyInit方法返回false),那么就获取一个connection,然后立即close。完成获取connection的测试。一旦发生异常,就将异常对象及信息对象传递给Hook对象(如果config中配置了Hook)
4. 判断config是否启用了connection追踪功能,如果启用,就实例化一个队列,并赋给对应的字段。该队列用来观察是否有应该安全关闭却没有关闭的connection。
5. 调用java.util.concurrent.Executors的newCachedThreadPool方法,创建一个线程池,并赋给对应的字段。该线程池用于异步地创建connection。
6. 根据config中配置的分区数量,创建ConnectionPartition数组。
7. 如果config中设置了使用独立的线程来关闭connection,那么就调用java.util.concurrent.Executors的newFixedThreadPool方法,创建一个线程池,并赋给对应的字段。
8. 调用java.util.concurrent.Executors的newScheduledThreadPool方法,创建一个线程池,并赋给相应字段。该线程池用于定期地测试connection的活性(即用它发送一条简单的SQL),并关闭故障的connection。每个分区一个线程。
9. 再调用java.util.concurrent.Executors的newScheduledThreadPool方法,创建一个线程池,并赋给相应字段。该线程池用于定期地检查connection是否过期。每个分区一个线程。
10. 调用java.util.concurrent.Executors的newFixedThreadPool方法,创建一个线程池,并赋给相应字段。该线程池用于观察每个分区,根据需要动态地创建新的connection或清理过剩的。
11. 如果开启了对close操作的监控,就会执行Executors.newCachedThreadPool来创建一个线程池,监控那些失败的close操作。
12. 创建各个分区(for循环)及相关步骤
 实例化分区(ConnectionPartition),并填充到分区数组
 同时实例化TransferQueue,将该数据结构注入到ConnectionPartition。注,TransferQueue是jsr166y中规定的标准接口,在jsr166y中提供了一种实现,即LinkedTransferQueue。BoneCP提供了两种实现,即LIFOQueue和BoundedLinkedTransferQueue。根据config中的配置选择相应的数据结构,后面的章节会对这些Queue进行详细的分析。
 完成队列的注入之后,再次判断config.isLazyInit(),如果返回false,就直接为每个分区添加connection对象(满足最小连接数)。注,这里使用的connection对象是com.jolbox.bonecp.ConnectionHandle的实例。这个类是对JDBC connection的包装,它实现了java.sql.Connection接口。通过包装来完成对脱离队列的connection的管理。该包装类中持有原pool对象和partition对象。
 启动线程池(根据config中的配置进行判断)
13. 再次初始化并启动一个线程池,用来提供释放statement的辅助线程(如果config中配置的该属性大于0)。
14. 如果激活了JMX,就执行initJMX()

2.4  BoneCP的getConnection()过程分析
1. 判断自己是否关闭(即调用了shutdown方法),如果发现关闭,就抛出SQLException
2. 获取当前线程ID,按分区数量对该值取模,计算出要访问的分区数组下标。
3. 按下标获取分区对象(ConnectionPartition),然后从分区持有的队列中poll出一个空闲的connection对象(类型为ConnectionHandle,它包装了JDBC connection)。
4. 如果poll的结果为null,说明该分区的队列中没有空闲的connection,那么从分区数组的0号开始轮询每个分区,直到poll出一个非null的connection。(循环结束之后仍有可能为null)
5. 判断分区的connection是否达到了上限,如果没有,就向队列中插入一个新的connection
6. 如果得到的结果还是为null,那么就调用分区的“阻塞一定时间的poll方法”,当该poll方法执行结束,仍然为null,就抛出SQLException
7. 调用ConnectionHandle对象的renewConnection方法,将该对象标记为“打开(也就是设置一个boolean变量)”。这一方法会将当前线程设置进ConnectionHandle对象。
8. 判断该ConnectionHandle对象是否持有一个Hook,如果有,就执行Hook的onCheckOut方法,并将该ConnectionHandle对象作为参数传入。Hook对象用以监听connection的生命周期。
9. 如果配置了closeConnectionWatch属性为true,就开启一个线程来监听ConnectionHandle对象的close操作。这是一种用于debug的功能。
10. 最后将该ConnectionHandle对象作为java.sql.Connection类型返回。

2.5  BoneCP的shutdown
1. 设置poolShuttingDown属性为true,表明该对象已经关闭,如果有线程继续调用getConnection方法,将抛出SQLException
2. 设置shutdownStackTrace属性(将当前线程的堆栈轨迹拼装为一个String),这个字符串将作为SQLException的参数传入
3. 关闭所有线程池
4. 在lock环境下关闭所有连接,并记录一些统计信息。如果ConnectionHandle设置了ConnectionHook,就调用Hook的onDestroy方法。


3.  BoneCP使用的队列数据结构分析

3.1  LIFOQueue
LIFOQueue继承自java.util.concurrent.LinkedBlockingDeque,借助父类的API来实现TransferQueue接口。并重写相应的方法,从FIFO队列变成了LIFO队列。所以,对该数据结构的分析主要集中在LinkedBlockingDeque上。

3.2  LinkedBlockingDeque(JDK1.6)
 它基于双向双端链表,持有头节点和尾节点的引用。节点(Node)是一个静态内部类,它是存放队列元素的最小单位,并持有上一个节点和下一个节点的引用。
 具有固定的容量。在构造时,一旦指定容量,将无法插入更多元素(容量字段capacity具有final修饰符)。如果无法预计容量的上限,那么可以使用默认的构造方法(它使用Integer.MAX_VALUE作为容量值)。
 该实现通过一个ReentrantLock对象和两个Condition对象来实现“阻塞”和“同步”。以下是这几个字段的介绍:
private final ReentrantLock lock = new ReentrantLock();
这把“可重入锁”用于对链表进行添加删除的方法,避免并发问题。

private final Condition notEmpty = lock.newCondition();
执行take操作的“阻塞”方法调用notEmpty.await()方法“等待链表非空”,当链表插入元素后,会调用notEmpty.signal()方法,唤醒某个被notEmpty阻塞的方法。

private final Condition notFull = lock.newCondition();
执行put操作的“阻塞”方法调用notFull.await()方法用于“等待链表未满”,当链表删除元素后,会调用notFull.signal()方法,唤醒某个被notFull阻塞的方法。

 上面介绍的是强制的阻塞,该队列通过Condition的awaitNanos(long)方法实现了超时阻塞。注,BoneCP中使用了超时阻塞,而没有使用强制阻塞方法。
 该数据结构的其它部分都是普通的链表操作,就不在本文中叙述。



3.3  BoundedLinkedTransferQueue
BoundedLinkedTransferQueue继承了LinkedTransferQueue,通过限定容量来实现了该类的“有界(bounded)”版本。它使用了原子变量java.util.concurrent.atomic.AtomicInteger来保存链表的大小。并在offer方法中调用ReentrantLock的lock方法。如果没有在配置文件中指定容量,构造BoneCP时将会选用LinkedTransferQueue。下面对LinkedTransferQueue进行关键部位的分析。

3.4  LinkedTransferQueue(将纳入JDK1.7)
该类是jsr166y提供的TransferQueue接口的实现。
 它基于单向双端链表,与上面介绍的LinkedBlockingDeque的区别是,它的Node仅持有其下一个节点的引用。这是一个典型FIFO队列。
 该实现类的一个最大的特点是,所有的队列基本操作都是去调用一个私有方法:
private E xfer(E e, boolean haveData, int how, long nanos) { … }
该方法较为复杂,它的执行流程将在后面详细描述。
 该类中,全部以CAS(比较并交换)方式进行字段的赋值和链表的操作。通过第三方的sun.misc.Unsafe类来实现CAS模式的无锁算法。所以,在整个类中,你是看不到对字段的赋值操作。
 4个特殊的静态变量语义(这些变量作为xfer的第三个参数传入,在不同的队列方法中指定,比如poll,put等):
NOW 用于不带超时的poll和tryTransfer方法
ASYNC 用于offer,put和add方法
SYNC 用于transfer和take方法
TIMED 用于带超时的poll和tryTransfer方法
 该类中,静态内部类Node的实现较为复杂。具有4个字段:
final boolean isData;
该布尔值用来判断
volatile Object item;
存放插入的元素
volatile Node next;
下一个节点的引用
volatile Thread waiter;
存放当前线程,当执行LockSupport.park(this)时,当前线程被阻塞,当执行LockSupport.unpark(node.waiter)时,该节点对应的线程将解除阻塞

3.4.1  LinkedTransferQueue元素插入过程分析
(调用offer(E e)方法,或put)
 插入第一个节点
1. 执行xfer(e, true, ASYNC, 0);
2. 由于是第一次插入,头节点为null,就跳过for循环
3. 判断第三个参数how!=NOW,进入if块
4. 创建一个新节点Node,将插入的元素和第二个参数传入构造方法
5. 然后调用私有方法tryAppend,将Node和第二个参数传入
6. 经过一些判断,tryAppend方法调用casHead方法,以CAS的方式将Node赋值给head字段(头节点)。
 插入第二个节点
1. 同样,执行xfer(e, true, ASYNC, 0);
2. 进入for循环,取得头节点的引用。但在第一次循环时,判断该节点的isData字段等于第二个参数,就跳出循环。
3. 判断第三个参数how!=NOW,进入if块
4. 创建一个新节点Node,将插入的元素和第二个参数传入构造方法
5. 调用tryAppend方法。发现尾节点为null,而头节点的next节点也为null,就调用头节点的casNext方法,将新的Node节点以CAS的方式赋值给头结点的next字段。然后调用casTail方法,将tail字段指向这个新的节点。以上过程都是经过了一系列的if判断,并在判断的过程中有赋值操作,流程较为复杂。
6. 至此,链表结构已经形成,并且head和tail字段分别指向这两个节点。
 插入更多节点
1. 与插入第二个节点时的1,2,3,4步相同
2. 调用tryAppend方法,如果发现该tail节点的next节点为null,就以CAS的方式将它的next指向新的Node,最后,将tail字段指向这个新的节点。(注,这一过程结束之后,最后一个节点的next会指向自身。)


3.4.2  LinkedTransferQueue元素弹出过程分析
(调用poll方法,take方法稍有不同)
 第一次弹出节点
1. 执行xfer(null, false, NOW, 0);
2. 得到head节点的引用,如果其item字段(即插入的元素)不为null,就调用头节点的casItem方法,将item字段设置为null,如果设置成功,就直接return 这个item。
3. 此时,这个item为null的头节点尚未被清除。如果这时有插入操作发生,会通过一些判断措施找到后面的节点。
 第N次弹出节点
1. 同上面的第1步
2. 得到head节点的引用,发现其item字段为null,就得到next节点的引用,然后进入第二次循环(注,这些操作都是出于for循环中)。如果发现next节点的item不为null,就调用casItem方法,将这个item设置为null。
3. 然后,将head字段指向next的下一个节点,(即,前面两个空的节点都可以被垃圾回收了,因为是单向的链表),最后,return获取到的next节点的item,完成元素的弹出。

在实际代码中,上述插入和删除的过程是非常复杂的,包含了一系列的if判断和循环操作,比如,将tail指向新的节点这一过程是在tryAppend的第二次循环时实现的(假设中间一切正常,而如果出现并发问题,那么又会有别的处理方式),这些特殊的处理方式都是为了实现“无锁算法(lock-free)”。

4.BoneCP线程池
BoneCP提供了7种不同职能的线程池,其中有4种是必须的(构造时直接创建),另外3种是配置决定的。

对应字段 创建方式 必须
asyncExecutor Executors.newCachedThreadPool() YES
keepAliveScheduler Executors.newScheduledThreadPool(int,ThreadFactory) YES
maxAliveScheduler Executors.newScheduledThreadPool(int,ThreadFactory) YES
connectionsScheduler Executors.newFixedThreadPool(int,ThreadFactory) YES
releaseHelper Executors.newFixedThreadPool(ThreadFactory) NO
closeConnectionExecutor Executors.newCachedThreadPool(ThreadFactory) NO
statementCloseHelperExecutor Executors.newFixedThreadPool(int,ThreadFactory) NO

这些线程池几乎都是使用com.jolbox.bonecp.CustomThreadFactory来创建线程,目的是为了方便debug和捕获异常信息。通过该工厂,可以为线程赋予特定的名称(线程池的名称+config中配置的连接池的名称),并且可以记录相关的异常信息(工厂本身实现了UncaughtExceptionHandler接口,将自己传递给Thread)。

下面对每个线程池进行更详细的说明(以下若无特别说明,connection均代表ConnectionHandle类型)

asyncExecutor
该线程池用于异步地获取连接,BoneCP除了提供一个getConnection方法外,还提供了一个getAsyncConnection方法,它返回一个java.util.concurrent.Future对象,使用方式如下:
Future<Connection> result = pool. getAsyncConnection();
//do something else
Connection conn = result.get();
由于这种异步调用的频率不能确定(视应用而定),所以BoneCP选择了可以根据需要自动创建新线程的ThreadPool,该线程池对线程具有60秒的缓存时限

keepAliveScheduler
该线程池创建了分区数量的线程,定期(这个间隔时间是可配置的)检查分区中的connection,判断它的空闲时间是否超过了指定的时间,判断其是否仍然可用(发送一条简单的SQL)。将不符合要求的connection关闭。每次对connection的检查都是“出列”的操作,当检查完毕,如果该connection符合要求,就将它放回分区队列中,并且尝试调用TransferQueue的tryTransfer方法将其送给正等待获取元素的线程。

maxAliveScheduler
该线程池创建了分区数量的线程,定期(这个间隔时间是可配置的)检查分区中的connection,判断其是否超出了最大存活时间。将过期的connection关闭。检查符合要求(没有过期),就放回队列。主要操作类似于keepAliveScheduler。



connectionsScheduler
该线程池为每个分区指定了一个线程,当connection不够用时,这些线程就会创建新的connection来弥补自己分区的连接数(前提是不超过上限)。作者在这里用到一个技巧,就是使用了阻塞队列来阻塞这些线程,而无需使用sleep或其它方法(参见ConnectionPartition中的poolWatchThreadSignalQueue字段,该字段仅有一个get方法)。本线程池中的线程都会调用该队列的take方法阻塞自己。当有需要时,就会向该队列中插入一个元素,这样,线程就会take完毕,阻塞结束,开始创建新的connection,而下一次循环又会被阻塞(注,线程的run方法中用了死循环,并且那个阻塞队列定容为一个元素,所以,take之后,队列size为0,下一次take又会被阻塞)


releaseHelper
该线程池用于异步地关闭connection,这样能更快速的响应调用方。每个分区对应若干个这样的线程(数量可配置),这些线程在分区实例化时启动,并使用了一个阻塞队列的take方法阻塞自己(参见ConnectionPartition中的connectionsPendingRelease字段)。当关闭connection时,如果开启了releaseHelper,就会将connection加入该阻塞队列。被阻塞的线程就会解除阻塞,并关闭该connection,然后进入下一次循环…


closeConnectionExecutor
该线程池用于监控通过getConnection()得到connection的线程,如果该发现该线程结束后,该connection没有正确的关闭,将会记录日志。在线程的run方法中,使用了join来等待被监控的线程结束。个人认为这种观察结果并不十分准确,因为如果关闭connection的操作是异步去执行的,那么就不能及时地得到close的结果,应该在join之后设定少量的延时,以等待异步的完成。


statementCloseHelperExecutor
如果开启了这项功能,那么当statement关闭的时候,就会将该statement放入一个阻塞队列,这个线程池的线程发现阻塞队列有了元素,就会得到该元素,并执行关闭操作。原理与releaseHelper相同。

5.发现BoneCP-0.7.0中的bug
在BoneCP的构造方法中有一个作者的手误,我已经将这个错误通知了作者。该错误会影响debug和connection的回收。下面是BoneCP论坛交流的截图


BoneCP作者将maxAliveScheduler误写成了keepAliveScheduler,这就导致检查maxAge和检查idle的操作都去使用同一个线程池。作者已经FIXED该bug,请使用BoneCP的朋友换成下一个修订版

你可能感兴趣的:(java,多线程,数据结构 bonecp)