首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 数据库 > 其他数据库 >

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

2012-09-12 
数据库连接池BoneCP源码分析报告不错,弄上来一起学习。数据库连接池BONECP源码分析报告11. 简述21.1官方主

数据库连接池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的shutdown6
3.  BONECP使用的队列数据结构分析7
3.1  LIFOQueue7
3.2  LinkedBlockingDeque(JDK1.6)7
3.3  BoundedLinkedTransferQueue8
3.4  LinkedTransferQueue(将纳入JDK1.7)8
4.BONECP线程池10
5.发现BONECP-0.7.0中的BUG12













数据库连接池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种是配置决定的。

对应字段创建方式必须
asyncExecutorExecutors.newCachedThreadPool()YES
keepAliveSchedulerExecutors.newScheduledThreadPool(int,ThreadFactory)YES
maxAliveSchedulerExecutors.newScheduledThreadPool(int,ThreadFactory)YES
connectionsSchedulerExecutors.newFixedThreadPool(int,ThreadFactory)YES
releaseHelperExecutors.newFixedThreadPool(ThreadFactory)NO
closeConnectionExecutorExecutors.newCachedThreadPool(ThreadFactory)NO
statementCloseHelperExecutorExecutors.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的朋友换成下一个修订版

热点排行