HikariCP数据库连接池实战
上QQ阅读APP看书,第一时间看更新

2.3 数据库连接池百晓生《兵器谱》

《兵器谱》是古龙笔下的一本虚构的古籍,由小说中的人物“平湖”百晓生所著。古籍中列出了当时武林中人的兵器及武功的排名。在百晓生的《兵器谱》中,天机老人的天机棒、上官金虹的子母龙凤环、李寻欢的小李飞刀分别排名第一、第二、第三。与此类似,数据库连接池也是百花齐放、各有高下。我们可以用百晓生的《兵器谱》作为比喻,来收录、盘点各种各样的数据库连接池。

百度百科https://baike.baidu.com/item/数据库连接池/1518538中介绍了在Java中开源的常见数据库连接池主要有以下十一种。

❑c3p0:是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现JDBC3和JDBC2扩展规范说明的Connection和Statement池的DataSources对象。

❑Proxool:是一个Java SQL Driver驱动程序,提供了对选择的其他类型的驱动程序的连接池封装。可以非常简单地移植到现存的代码中,完全可配置,快速、成熟、健壮。可以透明地为现存的JDBC驱动程序增加连接池功能。

❑Jakarta DBCP:DBCP是一个依赖Jakartacommons-pool对象池机制的数据库连接池。DBCP可以直接在应用程序中使用。也许这就是Tomcat DBCP连接池,Tomcat默认使用的就是这个连接池。

❑DDConnectionBroker:是一个简单、轻量级的数据库连接池。

❑DBPool:是一个高效、易配置的数据库连接池。它除了支持连接池应有的功能之外,还包括了一个对象池,使用户能够开发一个满足自己需求的数据库连接池。

❑XAPool:是一个XA数据库连接池。它实现了javax.sql.XADataSource,并提供了连接池工具。

❑Primrose:是一个Java开发的数据库连接池。当前支持的容器包括Tomcat4&5、Resin3与JBoss3。它同样也有一个独立的版本,可以在应用程序中使用而不必运行在容器中。Primrose通过一个Web接口来控制SQL处理的追踪、配置,以及动态池管理。在重负荷的情况下可进行连接请求队列处理。

❑SmartPool:是一个连接池组件,它模仿应用服务器对象池的特性。SmartPool能够解决一些临界问题,如连接泄漏(connection leaks)、连接阻塞、打开的JDBC对象(如Statements、PreparedStatements)等。

❑MiniConnectionPoolManager:是一个轻量级JDBC数据库连接池。它只需要Java 1.5(或更高)即可,并且没有依赖第三方包。

❑BoneCP:是一个快速、开源的数据库连接池。帮用户管理数据连接,让应用程序能更快速地访问数据库。比c3p0/DBCP连接池速度快25倍。

❑Druid:它不仅是一个数据库连接池,还包含一个ProxyDriver、一系列内置的JDBC组件库、一个SQL Parser。支持所有JDBC兼容的数据库,包括Oracle、MySQL、Derby、Postgresql、SQL Server、H2等。

MySQL从One-Thread-Per-Connection进化到ThreadPool线程池模型,所以线程池可以作为MySQL进化发展的里程碑。数据库连接池有过类似的经历,比较有代表性的是Apache Commons DBCP,在1.X版本中是单线程模型设计,从2.X版本开始采用多线程模型。按照线程模型分类,我们可以初步把过往涌现的这些数据库连接池产品分为第一代数据库连接池和第二代数据库连接池。一般来说,c3p0、Proxool、XAPool和DBCP 1.X版本属于第一代数据库连接池;DBCP2.X版本、Tomcat JDBC Pool、BoneCP、Druid和HikariCP属于第二代数据库连接池。用版本发布时间区分也是区分两代产品一个比较偷懒的方法,靠近当代的数据库连接池大多会选择多线程模型。

有些数据库连接池已经湮灭在历史的演进中,而有些则“老当益壮,宁移白首之心,不坠青云之志”,有些专攻性能,有些主打全面。

在Java领域中开源的数据库连接池有很多,可以说是百家争鸣、百花齐放,但是目前还没有一本书专门进行收录和整理。本节博海拾贝,让我们一起去看看在数据库连接池技术的演进过程中,都出现了哪些数据库连接池,它们有什么特色,彼此之间都有什么联系。

2.3.1 c3p0

c3p0是Swaldman主要开发并维护的一款开源的数据库连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展来增强传统的JDBC。从版本0.9.5c3p0的0.9.5.2版本需要Java 1.6.x及以上版本支持。开始,c3p0完全支持JDBC4规范。它的GitHubc3p0 GitHub地址:https://github.com/swaldman/c3p0是这样描述的:“c3p0 is a mature, highly concurrent JDBC Connection. pooling library, with support for caching and reuse of PreparedStatements”。中文意思是,c3p0是一个成熟的、高并发的JDBC连接池库,支持缓存和重用PreparedStatements。

c3p0可以说是数据库连接池界的老古董,在很长一段时间内它甚至就是数据库连接池的代名词。当年盛极一时的Hibernate和Spring都曾将其作为内置的数据库连接池,它的稳定性可见一斑。c3p0的愿景是它提供的DataSource可以适合更多的J2EE企业应用程序使用,因此坚持修复问题、不断迭代版本。c3p0代码与它的竞争对手相比,代码量巨大且结构复杂,它需要分析120个类(Vibur为34个类,HikariCP为21个类),但也正因为这个它基本包含了数据库连接池的所有功能。与无锁设计的HikariCP和Vibur-DBCP不同的是,c3p0具有超过230个synchronized同步块和方法,在不同的类中充斥着大量wait()及notifyAll()方法,这些导致死锁倾向的代码造成了在网络上搜索“c3p0死锁”可以查到大量的资料。由于代码量复杂等原因,c3p0在基准测试中也始终排在最后。c3p0在默认情况下不会在getConnection的时候测试连接可用性,这点也是不安全的默认配置。

特别的,c3p0提供了一些有用的服务:

1)一个将传统的基于DriverManager的JDBC驱动程序调整为较新的javax.sql. DataSource方案的类,以获取数据库连接。

2)基于DataSources的Connection和PreparedStatements的透明池,可以“包装”传统驱动程序或任意非池化数据源。

它早在2012年就诞生了,可惜的是,自从2015年12月9日发布c3p0的0.9.5.2版本c3p0版本更新信息见https://github.com/swaldman/c3p0/blob/master/src/dist-static/CHANGELOG.以后,它就销声匿迹了。在很长一段时间内,我一直认为它是一个已经死去的数据库连接池,在本书的初稿中我也将它列为“死亡者”名单。但是在修订本章的时候,我无意中在c3p0的仓库里发现,在2019年1月27日0.9.5.3版本又默默地更新了,如图2-5所示。

图2-5 c3p0版本信息

0.9.5.3版本主要根据GitHub的用户建议做了两件事,一件是将mchange-commons-java升级到0.2.15版本并支持Log4j2,另一件是不再扩展XML配置文件中的实体引用这项改动来自https://nvd.nist.gov/vuln/detail/CVE-2018-20433,由GitHub用户zhutougg提出。(由于config属性的覆盖行为会引发安全问题)。

从2015年12月9日到2019年1月27日整整3年多没有更新了,这虽然是不大的改造,但却是非常有意义的版本升级,至少证明作者Swaldman并没有完全放弃这款数据库连接池。

c3p0在sourceforge中也托管了其项目信息,c3p0在sourceforge托管项目信息https://sourceforge.net/projects/c3p0/如果想了解更多关于c3p0相关概念、使用方式、配置、高级特性、性能等,用户可以观看在GitHub下载的c3p0包中的doc目录下的index.html,当然也可以登录网址https://www.mchange.com/projects/c3p0/在线观看,它们的内容是一致的,如图2-6所示。

图2-6 C3P0在线网址

c3p0库在以下细节上进行了打磨以确保正确性:

1)DataSources都是可引用和可序列化的,因此其适合绑定到各种基于JNDI的命名服务。

2)当引入Connection和Statement时,都会仔细清理Statement和ResultSets,这是为了防止客户端使用Lazy模式。但常见的资源管理策略仅仅清理Connection而造成资源耗尽。

3)该库采用JDBC 2和JDBC 3规范定义的方法(即使这些方法与库作者的首选项冲突)。DataSources以JavaBean样式编写,提供所有必需和大多数可选属性(以及一些非标准属性)和无参构造函数。实现了所有JDBC定义的内部接口(ConnectionPoolDataSource, PooledConnection, ConnectionEvent-generating Connections等)。用户可以将c3p0类与兼容的第三方实现混合使用(尽管并非所有c3p0功能都可以与ConnectionPoolDataSource的外部实现一起使用)。

c3p0的设计非常简单易用。要使用c3p0需要下载其jar包,在c3p0的jar包中共有3个包。以0.9.5.2版本为例,如果使用非Oracle数据库,则只需导入c3p0-0.9.5.2jar包和mchange-commons-java-0.2.11.jar包即可;如果使用Oracle数据库的话,还需要导入c3p0-oracle-thin-extras-0.9.5.2.jar包。当然,目前更新了0.9.5.3版本,若使用最新版本,对于非Oracle的数据库,只需要引入c3p0-0.9.5.3.jar和mchange-commons-java-0.2.15.jar。

接着可以创建一个如下的DataSource:

        import com.mchange.v2.c3p0.*;
        ...
        ComboPooledDataSource cpds = new ComboPooledDataSource();
        cpds.setDriverClass( "org.postgresql.Driver" ); //loads the jdbc driver
        cpds.setJdbcUrl( "jdbc:postgresql://localhost/testdb" );
        cpds.setUser("dbuser");
        cpds.setPassword("dbpassword");

若打开PreparedStatement,需设置maxStatements或maxStatementsPerConnection(默认0)。

        cpds.setMaxStatements( 180 );

用户可以用这个根据默认参数配置的连接池执行任意你想通过DataSource执行的操作,也可以将DataSource绑定到JNDI名称服务。当操作执行完毕后,用户可以如下清理创建的DataSource:

        cpds.close();

就是这么简单,剩下的都是细节。用户可以根据上面提到的mchange文档和c3p0的API文档https://www.mchange.com/projects/c3p0/apidocs/index.html深入研究。从用户的角度来看,c3p0只提供标准的JDBC DataSource对象,但是创建这些DataSource时,用户可以控制与池相关、命名相关的属性和其他属性。配置属性的完整参阅官网附录A https://www.mchange.com/projects/c3p0/#configuration_properties。

为了更加方便读者的理解,我也写了一个基于maven的c3p0示例demo。示例代码托管在https://github.com/CharlesMaster/c3p0-Demo。该demo托管在GitHub,读者可以按本页脚注获取。简单说一下该案例,该工具类一加载进内存就利用c3p0的连接池类ComboPooledDataSource的对象来设置各个数据库驱动和连接池的参数,例如数据库连接驱动、数据库URL、数据库用户名和密码、连接池里的最大和最小连接数、连接池初始化时的连接数等。这些配置只是ComboPooledDataSource对象中设置方法的冰山一角,我们还可以通过ComboPooledDataSource对象的方法为连接池设置更多的功能和参数。

ComboPooledDataSource是操作数据库连接池的关键类,获取数据库的连接可以通过ComboPooledDataSource的getConnection()方法,释放数据库的连接则是通过连接对象的close方法。释放资源时(调用Connection对象的close方法)不会将连接销毁,而是重新放入c3p0的连接池中。

运行示例代码中的测试代码,在控制台上可以看到c3p0连接池获得的Connection对象,如图2-7所示。

图2-7 c3p0测试代码控制台内容输出

图2-7中的信息是c3p0在创建数据库连接池时通过日志记录的信息,HikariCP等其他数据库连接池也可以开启这些记录信息,以便于调试和问题排查。我们可以通过这些信息来查看数据库连接池创建时的情况。

c3p0官网上列出了其自认为的已知缺点,主要有两方面。

1)连接和语句基于每个身份验证进行池化。因此,如果一个池支持的DataSource用于获取[ user = alice, password = secret1]和[ user = bob, password = secret2]的连接,则会有两个不同的池,而DataSource可能在最坏的情况下管理maxPoolSize属性指定的连接数的两倍。

这一事实是DataSource规范定义的自然结果(允许通过多个用户身份验证获取Connections),并且要求单个池中的所有Connections在功能上相同。这个“问题”不会改变或修复。这里要注意的是你了解发生了什么。

2)Statement pooling的开销太高。对于未对PreparedStatements执行重要预处理的驱动程序,池化开销超过任何节省。因此,默认情况下应关闭语句池。如果用户的驱动程序确实预处理PreparedStatements,特别是如果它通过IPC与RDBMS这样做,用户可能会通过打开语句池来看到性能的显著提升(通过将配置属性maxStatements或maxStatementsPerConnection设置为大于零的值来执行此操作)。

这里,我再补充第3点,即“APPARENT DEADLOCK”的问题。c3p0在从连接池中获取和返回连接的时候,采用了异步的处理方式,使用一个线程池异步将返回关闭了(没有真正关闭)的连接放入连接池中。这是一个非常严重的Bug,也是一个著名的c3p0问题,当高并发的时候会出现严重的性能问题。原因是,调用了上文中c3p0获取的连接的close方法是异步的,异步就是将连接放入一个事件队列中等待内部进行处理,而不是立即放入数据库连接池中。c3p0中的AcquireTask(获取任务)就会大量占用内部线程池,导致没有足够的线程来将数据库连接池外使用完的连接放回池内。当然,用户可以自己写一个优先级最高的线程来单独、优先、迅速地归还连接。

c3p0的性能在众多连接池中属于比较低的,在BoneCP的官网上明确写到BoneCP比c3p0/DBCP连接池快25倍。c3p0与DBCP的一个主要区别就是,DBCP没有自动回收空闲连接的功能,而c3p0有自动回收空闲连接功能。本书的主人公HikariCP在这些方面就处理得非常精妙,除了它数据结构的定义以外,单独的HouseKeeper等都是精心打磨的。在HikariCP源码解析的章节中我们会详细介绍。

2.3.2 Proxool

Proxool也可以说同样是数据库连接池领域的老古董了,与c3p0一样,它也是托管在sourceforge下的一个开源项目。Proxool在sourceforge托管地址:http://proxool.sourceforge.net。Proxool是一个数据库连接池框架,也可以说是一个连接池的类库,它同样提供了JDBC驱动程序的透明的连接池封装。

Proxool以JDBC驱动的身份为用户提供透明的连接池服务,所以Proxool移植到现有代码中特别容易,用户可以轻松地使用JDBC API、XML或Java属性文件进行配置。Proxool在那个年代另辟蹊径,开创性地提供了连接池监控功能(这也是Druid数据库连接池后来主打的方向),便于发现连接泄漏的等性能情况及连接事件。它也符合J2SE API,让用户对于开发标准有信心。

Proxool稳定性好、健壮性高,它曾经和DBCP及c3p0一起,并列最为常见的3种JDBC连接池技术。Hibernate在hibernate-release-4.1.7.Final/lib/optional/proxool/中可以找到Proxool.jar,见证了这段历史。官方也曾经宣布,由于Bug太多不再支持DBCP,而推荐使用Proxool或c3p0。

Proxool创建于2001年,2008年8月23日是其最后一次更新版本,0.9.1是目前最新的一个版本。为什么不再维护?作者在GitHub上声称,更多的是作者个人原因,一方面从2006年起作者本人再也没有使用过Proxool,另一方面作者甚至不再使用Java了。作者提出愿意将Proxool交给新的维护者,可惜的是,这款存在了19年的Proxool很久没有更新并且不维护了,最近一次代码提交是两年前Billhorsman本人对README的一次不维护说明。风起云涌的数据库连接池江湖不断有新秀涌出,取而代之的有Druid、HikariCP等数据库连接池。我们不得不认为,Proxool已经夭折了。

由于历史悠久,Proxool当年做了JDK 1.2至1.5的兼容性测试。从0.8.0版本开始,使用了Cglib的代理库,用户普遍使用其最后更新的一个版本0.9.1版本,使用方式是在其官网http://nchc.dl.sourceforge.net/sourceforge/proxool/proxool-0.9.1-source.zip下载Proxool源码,下载完后解压,把proxool.jar和proxool-cglib.jar放入要配置的项目的lib目录下。它的很多设计理念都被HikariCP认可并吸收,HikariCP在继承过程中进行了独具匠心的打磨。例如,关于Cglib等字节码的代理,这也是HikariCP仔细打磨的地方,在HikariCP源码解析的章节我们会详细介绍。

Proxool的源码并不是很多,但是阅读起来非常有意思,完全可以认为它是HikariCP数据库连接池的前身版的手写实现。其核心的ProxoolFacade用于管理连接池的注册、移除及监听事件,可以理解为ProxoolFacade是Proxool框架对外的外观,通过这里暴露Proxool的各种操作及属性,会把外来的各种各样的ConnectionPool注册到ConnectionPoolManaager,还会注册listener及jmx等。在Proxool内部将连接池的注册任务实现在了HouseKeeperController中,我们需要记住这个HouseKeeper的名字,因为后面本书源码解析HikariCP部分也是使用了同样的名字做类似的初始化工作。Proxool的源码还有一个重要的功能是基于状态的管理。状态主要包括:连接池当前连接数及连接池配置的属性,比如可用空闲连接、最大连接数、最小连接数、连接的最大活跃时长等。当然,还涉及比较复杂的并发处理。这些也都是HikariCP同样需要面对的问题。

由于篇幅原因,本书不对Proxool源码做深入解析,感兴趣的读者可以阅读Proxool注册到连接池、关闭Connection及Connection真正关闭、HouseKeeper连接管理等三大模块的源码,这些都是HikariCP同样处理的部分,但是HikariCP在这些问题的处理上将细节打磨得更加精彩。

2.3.3 XAPool

XA是X/Open CAE Specification(Distributed Transaction Processing)模型中定义的TM(Transaction Manager)与RM(Resource Manager)之间进行通信的接口。Java中的javax.transaction.xa.XAResource定义了XA接口,它依赖数据库厂商对jdbc-driver的具体实现。在XA规范中,数据库充当RM角色,应用需要充当TM的角色,即生成全局的txId,调用XAResource接口,把多个本地事务协调为全局统一的分布式事务。

事务分为本地事务和分布式事务。了解分布式事务,往往需要了解ACID(Atomicity原子性、Consistency一致性、Isolation隔离性、Durability持久性)、CAP(对分布式应用而言,不可能同时满足C一致性、A可用性、P分区容错性,在保证P的前提下往往需要在C和A之间进行平衡,如图2-8所示)和BASE理论(Basically Available基本可用、Soft state软状态和Eventually consistent最终一致性)等概念。符合传统ACID的通常叫作刚性事务,满足BASE理论的最终一致性事务叫作柔性事务。

图2-8 CAP取舍

XAPoolXAPool项目主页:http://xapool.ow2.org。是一个XA数据库连接池,它实现了javax.sql.XADataSource,并提供了连接池工具。这是一款主打分布式事务的数据库连接池,它允许池对象,JDBC连接和XA连接。一般来说,如果一些老项目中打算使用JOTMJOTM(http://jotm.objectweb.org/)提供事务处理,支持两阶段提交(2PC)。来实现分布式的事务管理,一般都需要配合使用XAPool。JOTM最后更新日期是2010年,实现JTA事务管理第三方管理工具目前比较活跃的是AtomikosAtomikos项目主页:https://www.atomikos.com。和Narayana等。

Atomikos是一种通过SPI注入不同的第三方组件作为事务管理器实现XA协议。同样作为DataSource增强的Apache孵化器项目ShardingSphere也使用了Atomikos, ShardingSphere已经发布了弱XA事务、BED最大努力送达柔性事务,规划发布基于Atomikos和Narayana的XA事务、基于Apache Service Comb的Saga事务、TCC(Try-Confirm-Cancel)事务,如图2-9所示。感兴趣的读者可以关注Sharding-Sphere开源项目。Sharding-Sphere的GitHub开源地址:https://github.com/sharding-sphere/sharding-sphere。

图2-9 ShardingSphere事务一览

XAPool与之前介绍的c3p0、Proxool一样,都是第一代数据库连接池的陈品,最近一次正式版发布是14年前2005年3月5日的1.5.0版本,最近的一次beta版发布是13年前2006年12月19日的1.6.beta版本。由于已经不再更新,并且其硬绑定的老伙计JOTM也失去活力,我们也可以同样认为XAPool夭折了。

“人猿相揖别。只几个石头磨过,小儿时节。”

主流的第一代数据库连接池c3p0(2015年12月9日初步封版)、Proxool(2018年8月23日封版)、XAPool(2006年12月19日封版),都因为各种各样的原因消逝在历史长河中,但是它们所提出的理念、对数据库连接池领域的积极探索及当年杀出重围时的锐气,都曾经撑起了一个时代,并为第二代数据库连接池的发展标注了崭新的方位。

小窍门

整本书才刚刚开始,就已经介绍了很多的jar包。那么jar包的版本是怎样的呢?推荐两个不错的java类、jar包及其依赖查找网站,希望对大家有帮助。

https://mvnrepository.com

https://www.findjar.com

2.3.4 DBCP

DBCPDBCP是Apache下独立的数据库连接池组件,https://commons.apache.org/proper/commons-dbcp。是Apache下独立的数据库连接池组件,由于Apache的缘故,它可能是使用最多的开源数据库连接池,比如Jakarta commons-pool对象池机制,以及Tomcat中使用的连接池组件就是DBCP。作为Apache项目,DBCP在Apache的维基百科DBCP维基百科地址是https://wiki.apache.org/commons/DBCP。是这样描述的:数据库连接池DBCP组件可以用于需要池化的JDBC资源的应用程序。除了JDBC连接外,它还支持汇集Statement和PreparedStatement实例。

不同于前面3种第一代数据库连接池由于各种各样的原因夭折,DBCP依托强有力的Apache不断迭代、老而弥坚,它是前半只脚踩在第一代数据库连接池,后半只脚踩在了第二代数据库连接池的跨时代的产品。由于许多Apache项目都支持与关系数据库的交互,所以DBCP在Apache的生态圈中的影响十分广泛。

和其他数据库连接池不一样的是,Apache Commons DBCP并不是独立实现连接池功能的,它内部依赖于Commons中的另一个子项目Apache Commons Pool。数据库连接池中最核心的“池”,就是由Pool组件提供的,Apache Commons Pool决定着数据库连接池的整体性能。单独使用DBCP一般需要commons-dbcp.jar、commons-pool.jar两个jar包,通过下载源码,也可以整理出了一份Apache Commons DBCP和Apache Commons Pool的版本依赖关系表,如表2-1所示。

表2-1 版本依赖关系表

在Apache Commons DBCP的下载页面DBCP下载页面http://commons.apache.org/proper/commons-dbcp/download_dbcp.cgi。我们可以看到DBCP经历了1.2.2、1.3、1.4(第一代)、2.0.1、2.1、2.1.1、2.2.0、2.3.0、2.4.0、2.5.0(第二代)的版本变迁。其中DBCP对于Java和JDBC版本的支持大致是,2.5.0 for JDBC 4.2 on Java 8,2.4.0 for JDBC 4.1 on Java 7,1.4 for JDBC 4 on Java 6,1.3 for JDBC 3 on Java 1.4 or 5。通过观察DBCP的ChangeNote,DBCP Change Note http://commons.apache.org/proper/commons-dbcp/changes-report.html可以发现Apache Commons DBCP依赖的Apache Commons Pool决定了大版本的更新:从2002年到2014年漫长的十多年间,Pool停留在1.x版本。DBCP也就跟着停留在1.x版本,这个阶段就是DBCP的年代,我们也可以理解为其处于第一代数据库连接池的时间段。当然,这个漫长的年代也催生了Tomcat JDBC Pool的出现,我们在后面会介绍它。

在2014年3月,DBCP终于更新到了2.x版本,基于新的线程模型的数据库连接池让DBCP焕然一新重获新生,稳定性得到提升,性能也有了质的提升。Apache Commons Pool 2类库是对象池技术的一种具体实现,它的出现是为了解决频繁的创建和销毁对象带来的性能损耗问题。其原理就是建立一个对象池,池中预先生成了一些对象,需要对象的时候借用,用完后进行归还,对象不够时灵活地自动创建,对象池满后提供参数控制是阻塞还是非阻塞响应租借用。在SpringBoot 1.5.x版本中,数据库连接池的默认配置是Tomcat Pool → HikariCP → Commons DBCP → Commons DBCP2;然而在2.x版本中,HikariCP被提升为默认的数据库连接池,数据库连接池的默认配置顺序是HikariCP → Tomcat pool→ Commons DBCP2。这也是为什么Spring Boot在1.5.x版本中支持DBCP和DBCP2,而2.X版本中只支持DBCP2的一段历史由来。DBCP2的出现表明DBCP从第一代数据库连接池跨越到了第二代数据库连接池,线程模型是划分众多数据库连接池历史分代的一个参照物。

注意

关于DBCP的版本变更还有一段有趣的关于Tomcat的故事。Tomcat在7.0以前的版本都是使用commons-dbcp做为连接池的实现。DBCP饱受诟病,Tomcat作为DBCP的忠实拥护者,在单线程的1.X DBCP年代遭遇了太多的性能瓶颈后,没来得及等待DBCP2出现,就自行开发了一套数据库连接池Tomcat JDBC Pool。Tomcat 7.x的帮助文档明确提出tomcat-dbcp.jar包含了Commons DBCP和Commons Pool:

The default database connection pool implementation in Apache Tomcat relies on the libraries from the Apache Commons project. The following libraries are used:Commons

DBCP、Commons Pool。

These libraries are located in a single JAR at $CATALINA_HOME/lib/tomcat-dbcp.jar. However, only the classes needed for connection pooling have been included, and the packages have been renamed to avoid interfering with applications.

2.3.5 Tomcat JDBC Pool

Tomcat JDBC Pool在多个版本的官方文档上都定义为取代Apache Commons DBCP的连接池。

上面我们介绍DBCP 1.x到2.x的历史时提到了Tomcat JDBC Pool在DBCP 2.x没有出来以前取代了DBCP 1.x的版本,JDBC连接池org.apache.tomcat.jdbc.pool是Apache Commons DBCP连接池的一种替换或备选方案。对于熟悉Commons DBCP的人来说,转而使用Tomcat连接池是非常简单的事,当然从其他连接池转换过来也非常容易。

在《Tomcat 8权威指南》一书中曾经写到为什么需要一个新的连接池,原因如下:

1)Commons DBCP 1.x是单线程。为了线程安全,在对象分配或对象返回的短期内,Commons锁定了全部池。但注意,这并不适用于Commons DBCP 2.x。

2)Commons DBCP 1.x可能会变得很慢。当逻辑CPU数目增长,或者试图借出或归还对象的并发线程增加时,性能就会受到影响。高并发系统受到的影响会更为显著。注意,这并不适用于Commons DBCP 2.x。

3)Commons DBCP拥有60多个类,而tomcat-jdbc-pool核心只有8个类。因此为了未来需求变更着想,肯定需要更少的改动。我们真正需要的只是连接池本身,其余的只是附属。

4)Commons DBCP使用静态接口,因此对于指定版本的JRE,只能采用正确版本的DBCP,否则就会出现NoSuchMethodException异常。

5)当DBCP可以用其他更简便的实现来替代时,实在不值得重写那60个类。

6)Tomcat JDBC连接池无需为库本身添加额外线程,就能异步获取连接。

7)Tomcat JDBC连接池是Tomcat的一个模块,依靠Tomcat JULI这个简化了的日志架构。

8)使用javax.sql.PooledConnection接口获取底层连接。

9)防止饥饿。如果池变空,线程将等待一个连接。当连接返回时,池就将唤醒正确的等待线程。大多数连接池只会一直维持饥饿状态。

当然,Tomcat JDBC连接池还具有一些其他连接池实现的没有的特点:

1)支持高并发环境与多核/CPU系统。

2)接口的动态实现。支持java.sql与java.sql接口(只要JDBC驱动),甚至在利用低版本的JDK来编译时也支持。

3)验证间隔时间。我们不必每次使用单个连接时都进行验证,可以在借出或归还连接时进行验证,只要不低于我们所设定的间隔时间就行。

4)只执行一次查询。当与数据库建立起连接时,只执行一次可配置查询。这项功能对会话设置非常有用,因为你可能会想在连接建立的整个时段内都保持会话。

5)能够配置自定义拦截器。通过自定义拦截器来增强功能。可以使用拦截器来采集查询统计,缓存会话状态,重新连接之前失败的连接,重新查询,缓存查询结果,等等。由于可以使用大量的选项,所以这种自定义拦截器也是没有限制的,与java.sql/javax.sql接口的JDK版本没有任何关系。

6)高性能。后面将举例展示一些性能差异。

7)极其简单。它的实现非常简单,代码行数与源文件都非常少,这都有赖于从一开始研发它时,就把简洁当作重中之重。对比一下c3p0,它的源文件超过了200个(最近一次统计),而Tomcat JDBC核心只有8个文件,连接池本身则大约只有这个数目的一半,所以能够轻易地跟踪和修改可能出现的Bug。

8)异步连接获取。可将连接请求队列化,系统返回Future<Connection>。

9)更好地处理空闲连接。不再简单粗暴地直接关闭空闲连接,而是把连接仍然保留在池中,通过更为巧妙的算法控制空闲连接池的规模。

9)可以控制连接应被废弃的时间。当池满了即废弃,或者指定一个池使用容差值,发生超时就进行废弃处理。

10)通过查询或语句来重置废弃连接计时器。允许一个使用了很长时间的连接不会因为超时而被废弃。这一点是通过使用ResetAbandonedTimer来实现的。

11)经过指定时间后,关闭连接。与返回池的时间相类似。

12)当连接要被释放时,获取JMX通知并记录所有日志。它类似于remove-AbandonedTimeout,但却不需要采取任何行为,只需要报告信息即可。通过suspectTimeout属性来实现。

13)可以通过java.sql.Driver、javax.sql.DataSource或javax.sql.XADataSource获取连接。通过dataSource与dataSourceJNDI属性实现这一点。

14)支持XA连接。

除了以上特点以外,Tomcat JDBC还存在一些问题。比如默认配置也存在类似c3p0的问题,就是在getConnection的时候并不会默认测试连接可用性。

此外,Tomcat JDBC在JDBC规范上也存在一些不完全遵守的问题。默认也不会重置连接状态(如自动提交、事务隔离级别等),用户必须手动配置名为ConnectionState的JDBCInterceptor。在自动提交中,如果连接池配置了autocommit=false,就需要在自己的事务中执行连接有效性测试isValid(),否则使用者获取的连接有可能就在一个事务进行中;对于创建连接时可以在连接上运行的初始化initSQL也是如此,Tomcat不会在自己的事务中封装连接测试或initSQL。连接池应该在Connection返回到池时或从池中取出之前,调用clearWarnings()方法清除SQL警告,然而Tomcat JDBC也没有这么做。JDBC规范还规定,连接关闭时,所有没有关闭的、已经打开的Statements都应该自动关闭,但是默认情况下Tomcat JDBC并不会跟踪Statements,除非手动配置一个StatementFinalizer拦截。但是不幸的是,StatementFinalizer使用一组WeakReference对象跟踪Statements,当JVM受到gc压力时,在Tomcat有机会关闭这些语句之前,可能会对废弃的Statements进行垃圾收集,这可能导致资源的泄漏,但是只有在gc压力下才会发生,因此可能很难追踪。

2.3.6 BoneCP

BoneCP官方主页是http://jolbox.com/, API文档为http://jolbox.com/bonecp/downloads/site/apidocs/index.html。是一个快速、免费、开源的Java数据库连接池(即JDBC Pool)。如果你熟悉c3p0或者DBCP,那么也就知道它是用来干什么的。简单地说,这个代码库将为你管理数据库连接,让你的应用具有更快的数据库访问能力。在c3p0和DBCP已经存在的时代,BoneCP的出现就是为了追求极致,它几乎比下一个最快的连接池选项快25倍,而且BoneCP从不自旋锁定,因此它不会减慢应用程序速度。BoneCP也提供了完善的基准测试,图2-10和图2-11为BoneCP官网提供的部分基准测试数据。

图2-10 单线程基准测试对比图

图2-11 多线程基准测试对比图

单线程模式的基准测试,在1000000(100万)次的连接获取/释放连接请求下(获取和释放没有延迟)、数据库连接池大小设置为20~50、助手线程为1、分区数为1、获取连接增量为5的统一背景下,官方测量结果显示BoneCP的性能远远高于DBCP和c3p0,如图2-10所示。

多线程模式的基准测试,在500个线程且每个线程尝试进行100次连接的获取/释放(获取和释放没有延迟)、数据库连接池大小设置为5、助手线程为5、获取连接增量为5的统一背景下,官方测量结果同样显示BoneCP的性能远远高于DBCP和c3p0,如图2-11所示。

BoneCP可以说是极致数据库连接池的领军开源项目。它和HikariCP也是非常有渊源的,除了HikariCP捐赠了BoneCP几美金的故事以外,BoneCP在浪潮之巅功成身退,深藏功与名,将一身衣钵传给了HikariCP。在BoneCP的GitHub上,我们可以看到它上一次提交的时间是2015年6月25日,BoneCP的Readme上写着短短的一句“墓志铭” :BoneCP的GitHub地址:https://github.com/wwadge/bonecp。“BoneCP是一种Java JDBC连接池实现,通过最小化锁争用来为应用程序提供更高的吞吐量,从而实现高性能。它击败了较旧的连接池,如c3p0和DBCP,但现在被视为弃用。建议用户使用HikariCP。”

BoneCP的特点如下:

1)具有高可扩展性的快速连接池。

2)在connection状态改变时,可配置回调机制(钩式拦截器)。

3)通过分区(Partitioning)来提升性能。

4)允许用户直接访问connection或statement。

5)自动扩展pool容量。

6)支持statement caching。

7)支持异步地获取connection(通过返回一个Future<Connection>实现)。

8)以异步的方式施放辅助线程(helper threads),来关闭connection和statement,以获得高性能。

9)在每个新获取的connection上,通过简单的机制,执行自定义的statement(即通过简单的SQL语句来测试connection是否有效,对应的配置属性为initSQL)。

10)支持运行时切换数据库,而不需要停止(shut down)应用。

11)能够自动地回放(replay)任何失败的事务(如,数据库或网络出现故障等)。

12)支持JMX。

13)可以延迟初始化(lazy initialization)。

14)支持使用XML或property文件的配置方式。

15)支持idle connection timeouts和max connection age。

16)自动检验connection(是否活跃等等)。

17)允许直接从数据库获取连接,而不通过Driver。

18)支持Datasouce和Hibernate。

19)支持通过debugging hooks来定位获取后未关闭的connection。

20)支持通过debugging来显示被关闭了两次的connection的堆栈轨迹(stack locations)。

21)支持自定义pool name。

22)代码整洁有序。

23)免费,开源,纯Java编写,具有完整的文档。

当然,BoneCP也存在一些问题。最大的一个问题是无法在getConnection()的时候配置数据库连接池来测试连接。然而其他每个数据库连接池大多都可以这样配置。它这样做是为了提升速度,但却牺牲了可靠性。在默认配置方面,BoneCP也不会在Connection返回到池时或从池中取出之前通过Connection.clearWarnings()方法清除SQL警告;默认情况下也不会关闭废弃的、已经打开的statements;也不会在自己的事务中封装连接测试或initSQL。

2.3.7 Druid

Druid是一个开源项目,Druid的GitHub地址是https://github.com/alibaba/druid。其作者是阿里温少。主要的工作是设计和实现阿里巴巴应用监控系统Dragoon。Druid和Fastjson都是监控系统实现的副产品。

Druid是阿里巴巴公司唯一使用的数据库连接池,阿里云DRDSDRDS:https://www.aliyun.com/product/drds和阿里TDDL都采用了Druid,可支持“双十一”等最严苛的使用场景。Druid有一句口号是“为监控而生的数据库连接池”。经过多年开源积累,已经相对成熟的Druid收获了不小的知名度与口碑,并陆续成为很多技术团队解决方案中的关键环节。Druid持续增强监控功能,监控功能与阿里云相关监控产品对接。其中的Parser模块会剥离出来作为一个项目大力发展。

Druid是一个JDBC组件,包括基于Filter-Chain模式的插件体系、DruidDataSource高效可管理的数据库连接池、SQLParser三个组成部分。

Druid的主要功能如下:

❑替换DBCP和c3p0。Druid提供了一个高效、功能强大、扩展性好的数据库连接池。

❑可以监控数据库访问性能。Druid内置了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这有助于对线上数据库访问性能进行分析。

❑数据库加密。直接把数据库密码写在配置文件中是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。

❑SQL执行日志。Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,用户可以按需要选择相应的LogFilter,监控自己的应用的数据库访问情况。

❑扩展JDBC。如果用户对JDBC层有编程的需求,可以通过Druid提供的Filter机制,很方便地编写JDBC层的扩展插件。

Druid的项目背景是这样的:2010年开始,温少负责设计一个叫作Dragoon的监控系统,需要一些监控组件,监控应用程序的运行情况,包括Web URI、Spring、JDBC等。为了监控SQL执行情况,他做了一个Filter-Chain模式的ProxyDriver,缺省提供StatFilter。当时他还做了一个SQL Parser。老板说,不如我们来一个更大的计划,把连接池、SQL Parser、Proxy Driver合起来做成一个项目,命名为Druid。于是Druid就诞生了。

Druid支持所有JDBC兼容的数据库,包括Oracle、MySQL、Derby、Postgresql、SQL Server、H2等。

Druid针对Oracle和MySQL做了特别优化,比如Oracle的PS Cache内存占用优化,MySQL的ping检测优化等。

Druid在DruidDataSourc和ProxyDriver上提供了Filter-Chain模式的扩展API,类似Serlvet的Filter,配置Filter拦截JDBC的方法调用。

在GitHub上我们可以看到有文档描述Druid是最好的数据库连接池,如图2-12所示。

图2-12 GitHub上对Druid的描述

为什么说它是最好的?最好体现在哪些方面?又是如何实现的?温少在一次访谈中是这样说的:

阿里巴巴是一个重度使用关系数据库的公司,我们在生产环境中大量使用Druid,通过长期在极高负载的生产环境中实际使用、修改和完善,Druid逐步发展成最好的数据库连接池。Druid在监控、可扩展性、稳定性和性能方面都有明显的优势。

1)强大的监控特性,通过Druid提供的监控功能,可以清楚地知道连接池和SQL的工作情况。

❑监控SQL的执行时间、ResultSet持有时间、返回行数、更新行数、错误次数、错误堆栈信息。

❑SQL执行的耗时区间分布。什么是耗时区间分布?比如,某个SQL执行了1000次,其中在0~1毫秒区间50次,在1~10毫秒800次,在10~100毫秒100次,在100~1000毫秒30次,在1~10秒15次,在10秒以上5次。通过耗时区间分布,能够非常清楚地知道SQL的执行耗时情况。

❑监控连接池的物理连接创建和销毁次数、逻辑连接的申请和关闭次数、非空等待次数、PSCache命中率等。

2)方便扩展。Druid提供了Filter-Chain模式的扩展API,可以自己编写Filter拦截JDBC中的任何方法,可以在上面做任何事情,比如说性能监控、SQL审计、用户名加密、日志等。

Druid内置了用于监控的StatFilter、日志输出的Log系列Filter、防御SQL注入攻击的WallFilter。

阿里巴巴公司内部实现了用于数据库加密的CirceFilter,以及与Web、Spring关联监控的DragoonStatFilter。

3)Druid集合了开源和商业数据库连接池的优秀特性,并结合阿里巴巴公司大规模苛刻生产环境的使用经验进行了优化。

❑ExceptionSorter。当一个连接产生不可恢复的异常时,例如Oracle error_code_28 session has been killed,必须立刻将其从连接池中逐出,否则会产生大量错误。目前只有Druid和JBoss DataSource实现了ExceptionSorter。

❑PSCache内存占用优化对于支持游标的数据库(如Oracle、SQL Server、DB2等,不包括MySQL)可以大幅度提升SQL执行性能。一个PreparedStatement对应服务器的一个游标,如果PreparedStatement被缓存起来重复执行,PreparedStatement没有被关闭,服务器端的游标就不会被关闭,那么性能提高会非常显著。在类似“SELECT * FROM T WHERE ID = ? ”这样的场景中,性能可能是一个数量级的提升。但在Oracle JDBC Driver中,其他的数据库连接池(DBCP、JBossDataSource)会占用过多内存,极端情况下可能大于1G。Druid调用OracleDriver提供管理PSCache内部API。

❑LRU是一个性能关键指标,特别是Oracle,其中每个Connection对应数据库端的一个进程,如果数据库连接池遵从LRU,则有助于数据库服务器优化,这是重要的指标。Druid、DBCP、Proxool、JBoss是遵守LRU的,而BoneCP、c3p0则不是。BoneCP在mock环境下性能可能还不错,但在真实环境中就不好了。

在性能方面:性能不是Druid的设计目标,但是测试数据表明,Druid性能比DBCP、c3p0、Proxool、JBoss都好。

在扩展性方面:Druid提供Filter-Chain模式的插件框架,通过编写Filter配置到DruidDataSource中,就可以拦截JDBC的各种API,从而实现扩展。Druid提供了一系列内置Filter。

在SQL注入方面:Druid的优势是在JDBC最低层进行拦截并判断,不会遗漏。Druid实现了Oracle、MySQL、Postgresql、SQL-92的Parser,基于SQL语法分析实现,理解其中的SQL语义,智能、准确、误报率低。

在迁移方面:从DBCP迁移到Druid连接池最方便,把org.apache.commons.dbcp. BasicDataSource修改为om.alibaba.druid.pool.DruidDataSource即可。Druid网站上提供了Druid/DBCP/c3p0/JBoss/WebLogic的参数对照表,通过这个对照表可以迁移用户目前的配置。