带你读《HikariCP数据库连接池实战》之二:数据库连接池江湖-阿里云开发者社区

开发者社区> 华章出版社> 正文

带你读《HikariCP数据库连接池实战》之二:数据库连接池江湖

简介: 本书不仅对市面上常见的连接池组件进行了全方位比较和分析,还以实战的角度深入介绍了高性能HikariCP连接池的使用、原理与维护。

点击查看第一章
点击查看第三章
第2章

数据库连接池江湖

本章将为读者介绍数据库连接池的一些基础概念。数据库连接池涉及的概念很多,这里介绍的是我认为在企业应用开发中非常基础和重要的概念。
数据库连接的建立是一种耗时长、性能低、代价高的操作,频繁地进行数据库连接的建立和关闭会极大影响系统的性能,若多线程并发量很大,这样耗时的数据库连接就可能让系统变得卡顿。此外,数据库同时支持的连接总数也是有限的,达到上限后,后续线程发起的数据库连接就会失败。因此,数据库连接池是一种关键的、有限的、昂贵的资源,对于复杂的应用,如果频繁地建立、关闭连接,那么就会极大地影响系统的性能、伸缩性和健壮性。重用数据库连接最主要的原因是减少应用程序与数据库之间的创建和销毁TCP连接的开销,数据库连接池的概念应运而生。
本章提及的概念之间的联系并不是非常紧密,读者可以有选择性地阅读自己感兴趣的部分。

2.1 为什么使用数据库连接池

一些初学者可能会有这样的疑问:为什么要使用数据库连接池?一些中高级的朋友也许会问:数据库连接池本身就是一个很简单的东西,只是内存里放了一个存连接的池,使用的时候拿,用完了就归还,有什么好讲的,又有何意义,居然还能写出一本书来?我曾经也有这样的疑问,但是在写这本书的两年时间内,我认为数据库连接池麻雀虽小五脏俱全,了解它还是很有必要的。
如图2-1所示,数据库连接池存在于数据库和用户之间,是一种管理数据库连接的中间件。用户通过数据库连接池获取连接,从而使用数据库的资源,并执行通用的“增删改查”操作。
image.png
数据库连接池在百度百科中的定义是:数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接,来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。
那么为什么使用数据库连接池会明显提高数据库操作的性能呢?下面我们结合TCP的3次握手和4次挥手,对不使用数据库连接池和使用数据库连接池这两种场景分别进行介绍。
让我们一起重温大学岁月,那些年在学习编程语言的时候,为了让学生能够快速学会数据库增删改查操作,教材里基本上都采用不使用数据库连接池的讲解和代码示例。经典的JDBC连接数据库的步骤大致是这样的:加载JDBC驱动→创建数据库连接→创建preparedStatement→执行SQL语句→遍历结果集→关闭资源。这样的教科书式的流程可以通俗理解为:应用程序用DataSource用数据库驱动来创建一个数据库连接,再通过TCP进行数据库的读写,最后关闭数据库连接。
以访问MySQL为例,执行一个SQL语句的完整TCP流程共经历TCP 3次握手建立连接、MySQL 3次握手认证、SQL语句执行、MySQL关闭、TCP 4次挥手关闭连接这5个步骤,如图2-2所示。
这样的传统方式比较适合教科书,但是却不适合企业实际生产。它存在的主要问题如下所示。
1)创建连接和关闭连接的过程比较耗时,并发时系统会变得很卡顿。
2)数据库同时支持的连接总数是有限的,如果并发量很大,那么数据库连接的总数就会被消耗光,增加数据库的负载,新的数据库连接请求就会失败。这样就会极大地浪费数据库的资源,极易造成数据库服务器内存溢出、宕机。
image.png
3)为了执行一条SQL,却产生了很多我们并不关心的网络IO。
4)应用如果频繁地创建连接和关闭连接,会导致JVM临时对象较多,GC频繁。
5)频繁关闭连接后,会出现大量TIME_WAIT 的TCP状态(在2个MSL之后关闭),这点很棘手。这个问题在第1章中已经结合实践调优经验详细说明过。
6)应用的响应时间及QPS较低。
使用数据库连接池之后,最直接的改变就是除了第1次访问时需要建立连接以外,之后的访问基本上只需复用已有的连接,而不是重新建立一个。
数据库连接池带来的优点如下所示。
1)资源重用更佳。由于数据库连接得到复用,减少了大量创建和关闭连接带来的开销,也大大减少了内存碎片和数据库临时进程、线程的数量,使得整体系统的运行更加平稳。
2)系统调优更简便。由于频繁关闭连接会出现TCP大量TIME_WAIT状态,如第1章的案例描述,TIME_WAIT的调优非常烦琐。使用了数据库连接池以后,由于资源重用,大大减少了频繁关闭连接的开销,大大降低了TIME_WAIT的出现频率。当然,数据库连接池有它自己独特的配置参数,这些参数如何调优在本书的后续章节中会详细介绍。
3)系统响应更快。数据库连接池在应用初始化的过程中一般都会提前准备好一些数据库连接,业务请求可以直接使用已经创建的连接,而不需要等待创建连接的开销。初始化数据库连接配合资源重用,使得数据库连接池可以大大缩短系统整体响应时间。
4)连接管理更灵活。数据库连接池作为一款中间件,除了扮演有界缓冲的角色外,在统一的连接管理上同样可以做很多文章。用户可以自行配置连接的最小数量、最大数量、最大空闲时间、获取连接超时间、心跳检测等。另外,用户也可以结合新的技术趋势,增加数据库连接池的动态配置、监控、故障演习等一系列实用的功能。
Vlad Mihalcea在他的著作《High-Performance Java Persistence》中介绍过一个实验,可视化显示建立和关闭数据库连接的累积开销,他比较了打开和关闭4种不同的RDBMS与HikariCP数据库连接池的1000个数据库连接,如图2-3所示。可以看出,使用数据库连接池HikariCP解决方案以后,连接获取时间大大地缩短了。通过减少连接获取间隔,整个事务响应时间也会缩短。
image.png
综上所述,这就是我们要在实际生产中使用数据库连接池的理由。

2.2 数据库连接池原理

数据库连接池的原理是:在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数、最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。
了解完数据库连接池的原理,我们继续来看数据库连接池的构成。数据库连接池以连接池的管理为核心,主要支持连接池的建立和释放这两大核心功能。“麻雀虽小,五脏俱全”,数据库连接池还可以支持其他非常实用的功能。一款商用的数据库连接池、一款能够被开发者广泛使用的数据库连接池、一款能够在开源社区持续活跃发展的数据库连接池还必须再支持一些实用的功能,如并发(锁性能优化乃至无锁)、连接数控制(不同的系统对连接数有不同的需求)、监控(一些自身管理机制来监视连接的数量及使用情况等)、外部配置(各种主流数据库连接池官方文档最核心的部分)、资源重用(数据库连接池的核心思想)、检测及容灾(面对一些网络、时间等问题的自愈)、多库多服务(如不同的数据库、不同的用户名和密码、分库分表等情况)、事务处理(对数据库的操作符合ALL-ALL-NOTHING原则)、定时任务(如空闲检查、最小连接数控制)、缓存(如PSCache等避免对SQL重复解析)、异常处理(对JDBC访问的异常统一处理)、组件维护(如连接状态、JDBC封装的维护)等。综上,一款商用的成熟的数据库连接池的构成大致如图2-4所示。
image.png

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

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

  • c3p0:一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现JDBC3和JDBC2扩展规范说明的Connection 和Statement 池的DataSources 对象。
  • Proxool:一个Java SQL Driver驱动程序,提供了对选择的其他类型的驱动程序的连接池封装。可以非常简单地移植到现存的代码中,完全可配置,快速、成熟、健壮。可以透明地为现存的JDBC驱动程序增加连接池功能。
  • Jakarta DBCP:DBCP是一个依赖Jakarta commons-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.5
开始,c3p0完全支持JDBC4规范。它在GitHub是这样描述的:“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的透明池,可以“包装”传统驱动程序或任意非池化数据源。
c3p0早在2012年就诞生了,可惜的是,自从2015年12月9日发布c3p0的0.9.5.2版
本以后,它就销声匿迹了。在很长一段时间内,我一直认为它是一个已经消亡的数据库连接池,在本书的初稿中我也将它列入了“死亡者”名单。但是在修订本章的时候,我无意中在c3p0的仓库里发现,其在2019年1月27日的0.9.5.3版本中又默默地进行了更新,如图2-5所示。
0.9.5.3版本主要根据GitHub的用户建议做了两件事,一件是将mchange-commons-java升级到0.2.15版本并支持Log4j2,另一件是不再扩展XML配置文件中的实体引用(由于config属性的覆盖行为会引发安全问题)。
从2015年12月9日到2019年1月27日整整3年多没有更新,这次虽然是规模不大的改造,但却是一次非常有意义的版本升级,至少证明作者Swaldman并没有完全放弃这款数据库连接池。
c3p0在sourceforge中也托管了其项目信息,如果想了解更多关于c3p0的相关概念、使用方式、配置、高级特性、性能等,用户可以观看在GitHub下载的c3p0包中的doc目录下的index.html,当然也可以登录网址在线观看,它们的内容是一致的,如图2-6所示。
image.png
image.png
c3p0库在以下细节上进行了打磨以确保正确性:
1)DataSources都是可引用和可序列化的,因此其适合绑定到各种基于JNDI的命名服务。
2)当引入Connection和Statement时,都会仔细清理Statement和ResultSet,这是为了防止客户端使用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文档深入研究。从用户的角度来看,c3p0只提供标准的JDBC DataSource对象,但是创建这些DataSource时,用户可以控制与池及命名相关的属性和其他属性。
为了更加方便读者的理解,我也写了一个基于maven的c3p0示例demo。该demo托管在GitHub,读者可以按本页脚注获取。简单说一下该案例,该工具类一加载进内存就利用c3p0的连接池类ComboPooledDataSource的对象来设置各个数据库驱动和连接池的参数,例如数据库连接驱动、数据库URL、数据库用户名和密码、连接池里的最大和最小连接数、连接池初始化时的连接数等。这些配置只是ComboPooledDataSource对象中设置方法的冰山一角,我们还可以通过ComboPooledDataSource对象的方法为连接池设置更多的功能和参数。
ComboPooledDataSource是操作数据库连接池的关键类,获取数据库的连接可以使用ComboPooledDataSource的getConnection()方法,释放数据库的连接则是使用连接对象的close方法。释放资源时(调用Connection对象的close方法)不会将连接销毁,而是重新放入c3p0的连接池中。
运行示例代码中的测试代码,在控制台上可以看到c3p0连接池获得的Connection对象,如图2-7所示。
image.png
图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等都是经过精心打磨的。

2.3.2 Proxool

Proxool同样也可以说是数据库连接池领域的老古董了,与c3p0一样,它也是托管在sourceforge下的一个开源项目。Proxool是一个数据库连接池框架,也可以说是一个连接池的类库,它同样提供了JDBC驱动程序的透明连接池封装。
Proxool以JDBC驱动的身份为用户提供透明的连接池服务,所以Proxool移植到现有代码中特别容易,用户可以轻松地使用JDBC API、XML或Java属性文件进行配置。Proxool在那个年代另辟蹊径,开创性地提供了连接池监控功能(这也是Druid数据库连接池后来主打的方向),便于发现连接泄露等性能情况及连接事件。它也符合J2SE API,让用户对于开发标准建立了信心。
Proxool稳定性好、健壮性高,它曾经和DBCP及c3p0一起,并列为最常见的3种JDBC连接池技术。Hibernate官方也曾经宣布,由于Bug太多不再支持DBCP,而推荐使用 Proxool或c3p0。
Proxool创建于2001年,2008年8月23日是其最后一次更新版本,0.9.1是目前最新的一个版本。作者Billhorsman在GitHub上声称不再维护更多的是由于个人原因,一方面从2006年起作者本人再也没有使用过Proxool,另一方面作者甚至不再使用Java了。作者提出愿意将Proxool交给新的维护者,可惜的是,这款存在了19年的Proxool很久都没有更新维护,最近一次代码提交是两年前作者对README进行的一次不维护说明。风起云涌的数据库连接池江湖不断有新秀涌出,取而代之的有Druid、HikariCP等数据库连接池。我们不得不认为,Proxool已经被淘汰了。
由于历史悠久,Proxool当年曾做过JDK 1.2至1.5的兼容性测试。从0.8.0版本开始,使用了Cglib的代理库,用户普遍使用其最后更新的0.9.1版本,使用方式是在其官网下载Proxool源码,下载后解压,把proxool.jar和proxool-cglib.jar放入要配置项目的lib目录下。它的很多设计理念都被HikariCP认可并吸收,HikariCP在继承过程中进行了独具匠心的打磨。例如,关于Cglib等字节码的代理,这也是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理论的最终一致性事务叫作柔性事务。

image.png

XAPool是一个XA数据库连接池,它实现了javax.sql.XADataSource,并提供了连接池工具。这是一款主打分布式事务的数据库连接池,它支持池对象、JDBC连接和XA连接。一般来说,如果一些老项目中打算使用JOTM来实现分布式的事务管理,一般都需要配合使用XAPool。JOTM最后的更新日期是2010年,实现JTA事务管理第三方管理工具目前比较活跃的是Atomikos和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开源项目。
image.png
XAPool与之前介绍的c3p0、Proxool一样,都是第一代数据库连接池的陈品,最近一次正式版发布是2005年3月5日的1.5.0版本,最近的一次beta版发布是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://mvn.repository.com
https://www.findjar.com

2.3.4 DBCP

DBCP是Apache下独立的数据库连接池组件,由于Apache的缘故,它可能是使用最多的开源数据库连接池,比如Jakarta commons-pool对象池机制,以及Tomcat中使用的连接池组件就是DBCP。作为Apache项目,DBCP在Apache的维基百科中是这样描述的:数据库连接池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所示。
image.png
在Apache Commons DBCP的下载页面我们可以看到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,可以发现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。

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。
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是一个快速、免费、开源的Java数据库连接池(即JDBC Pool)。如果你熟悉c3p0或DBCP,那么也就知道它是用来干什么的。简单地说,这个代码库将为你管理数据库连接,让你的应用具有更快的数据库访问能力。在c3p0和DBCP已经存在的时代,BoneCP的出现就是为了追求极致,它几乎比下一个最快的连接池选项快25倍,而且BoneCP从不自旋锁定,因此它就不会减慢应用程序速度。BoneCP也提供了完善的基准测试,图2-10和图2-11为BoneCP官网提供的部分基准测试数据。
image.png
image.png
单线程模式的基准测试,在1 000 000(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是一种Java JDBC连接池实现,通过最小化锁争用来为应用程序提供更高的吞吐量,从而实现高性能。它击败了较旧的连接池,如c3p0和DBCP,但现在被视为弃用。建议用户使用HikariCP。”
BoneCP的特点如下:
1)具有高可扩展性的快速连接池。
2)在connection状态改变时,可配置回调机制(钩式拦截器)。
3)通过分区(Partitioning)来提升性能。
4)允许用户直接访问connection或statement。
5)自动扩展pool容量。
6)支持statement caching。
7)支持异步地获取connection(通过返回一个Future实现)。
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是一个开源项目,其作者是阿里温少。温少经常主要的工作是设计和实现阿里巴巴应用监控系统Dragoon。Druid和Fastjson都是监控系统实现的副产品。
Druid是阿里巴巴唯一使用的数据库连接池,阿里云DRDS和阿里TDDL都采用了Druid,可支持“双十一”等最严苛的使用场景。Druid 有一句口号是“为监控而生的数据库连接池”。经过多年开源积累,已经相对成熟的 Druid 收获了不小的知名度与口碑,并陆续成为很多技术团队解决方案中的关键环节。Druid 持续增强监控功能,监控功能与阿里云相关监控产品对接。其中的 Parser 模块会剥离出来作为一个项目大力发展。
Druid是一个JDBC组件,由基于Filter-Chain模式的插件体系、DruidDataSource 高效可管理的数据库连接池、SQLParser 3个部分组成。
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所示。
image.png
为什么说它是最好的?最好体现在哪些方面?又是如何实现的?温少在一次访谈中是这样说的:“阿里巴巴是一个重度使用关系数据库的公司,我们在生产环境中大量使用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配置编写到Druid-
DataSource中,就可以拦截JDBC的各种API,从而实现扩展。Druid提供了一系列内置Filter。
SQL注入方面:Druid的优势是在JDBC最底层进行拦截并判断,不会遗漏。Druid实现了Oracle、MySQL、Postgresql以及SQL-92的Parser,基于SQL语法分析实现,理解其中的SQL语义,智能、准确、误报率低。
迁移方面:从DBCP迁移到Druid连接池最方便,把org.apache.commons.dbcp.Basic-
DataSource修改为om.alibaba.druid.pool.DruidDataSource即可。Druid网站上提供了Druid/DBCP/c3p0/JBoss/WebLogic的参数对照表,通过这个对照表可以迁移用户目前的配置。

2.4 主流数据库连接池对比

英国前首相温斯顿·丘吉尔曾经说过:“批评也许并不会令人满意,但却是必要的。它和人体疼痛的功能类似,它可以唤起人们对于事物不健康状态的关注。”近几年,手机评测比较热门,眼花缭乱的数据库连接池当然也需要一个评测。那么该如何评测呢?评价原则是:性能固然是好的,但是却不能以放弃可靠性为代价。

2.4.1 性能对比

在HikariCP的官网上,通过JMH Benchmarks对主流数据库连接池分别进行了Connection和Statement级的性能对比,如图2-13所示。可以看到,常见的数据库连接池有c3p0、DBCP2、Tomcat、Vibur、Hikari等,其中HikariCP的性能独占鳌头。
image.png

2.4.2 代码复杂度

虽然代码行数并不直接表明复杂性,但冗长和可理解性之间存在着无可争辩的相关性。一个被业内广泛接受的观点是:每千行代码的Bug数量在不同的项目和语言中表现得相当一致。除了测试套件外,目视检查代码中的逻辑错误和竞争条件是最可靠的方法之一。HikariCP从未经历过记录的死锁(deadlock)和活锁(livelock)的情况。
我们按照截至2014年3月15日已发布的主流数据库连接池版本,不包括测试和comment,从文件数和代码量上,我们可以得到对比表格,如表2-2所示。
image.png
上述数据来源于Brett Wooldridge 2014年写的文章。写到这里,不妨和读者分享一个小故事。笔者曾经在和朋友的闲聊过程中分享这组数据,笔者的朋友王振飞先生在2018年4月26日的时候也使用当时最新的TAG对Druid和HikariCP的数据做了一次比较。得到的结论如下:
1)只统计Java文件和xml文件,Druid总行数430289,HikariCP总行数18372。
2)只统计Java代码,Druid总行数428749,HikariCP总行数17556。
3)再过滤一下test目录,Druid总行数215232,HikariCP总行数7960。
4)不考虑性能,光一个DruidDataSource就编写了3000行。
一个在某购物网站用户增长组的好友也曾经告诉我,当他用HikariCP替换Durid之后,RT出现断崖式下滑(下降至1.2~1.5毫秒),并且持续稳定、毛刺少。他还给我分享了当时的性能测试报告。目前,据我所知,“杭州有赞”“51信用卡”“海康威视”3家公司都采用HikariCP作为数据库连接池,很多中小型互联网企业也选择HikariCP作为数据库连接池。

2.4.3 功能对比

在Druid的官方文档上,也给出了一份几种数据库连接池功能的对比表,如表2-3所示。
image.png
image.png
在这份文档中,Druid强调了如下几个参数。
1)LRU。LRU是一个性能关键指标,特别是Oracle,其中每个Connection对应数据库端的一个进程,如果数据库连接池遵从LRU,有助于数据库服务器优化,这是重要的指标。在测试中,Druid、DBCP、Proxool遵守LRU,BoneCP、c3p0则不遵守。BoneCP在mock环境下性能可能不错,但在真实环境中则不见得优秀。
2)PSCache。PSCache是数据库连接池的关键指标。在Oracle中,类似SELECT NAME FROM USER WHERE ID = ?这样的SQL,启用PSCache和不启用PSCache的性能可能会相差一个数量级。Proxool是不支持PSCache的数据库连接池,如果你使用Oracle、SQL Server、DB2、Sybase这样支持游标的数据库,那就完全不用考虑Proxool。
3)PSCache-Oracle-Optimized。在Oracle 10系列的Driver中,如果开启PSCache,则会占用大量内存,必须做特别的处理,比如启用内部的EnterImplicitCache等方法优化才能减少内存的占用。这个功能只有DruidDataSource有。如果你使用的是Oracle JDBC,则应该毫不犹豫地采用DruidDataSource。
4)ExceptionSorter。ExceptionSorter是一个很重要的容错特性,如果一个连接产生了一个不可恢复的错误,必须立刻将其从连接池中去掉,否则会连续产生大量错误。这个特性,目前只有JBossDataSource和Druid实现。Druid的实现参考自JBossDataSource,经过长期生产反馈进行了补充。
结合HikariCP和Druid的官方文档,并收集整理了大量数据库连接资料,笔者从线程同步、PSCache、LRU、ExceptionSorted、监控、扩展性、SQL拦截及解析、代码复杂度、特点、连接池管理、JNDI、Tomcat数据源、更新维护等角度对主流数据库连接池做了一份更全面的选型对比,以方便读者在实际学习工作过程中作为工具使用,如表2-4所示。

2.4.4 数据库中断

在HikariCP的官方文档上,记载了关于对比主流数据库连接池数据库中断的测试情况,这是针对各种数据库连接池都提供的超时功能的测试对比。这个测试实验模拟了这样一个场景,将数据库连接池执行getConnection()在5秒的调用后超时,应用程序应在指定时间内获得连接异常。该实验设置了一个测试,可以针对远程MySQL服务器配置4个池(HikariCP、c3p0、DBCP2、Vibur)。每隔2秒getConnection()调用一次,执行查询,然后关闭连接(返回池)。
image.png
每个连接池都如表2-5所示进行了配置,来验证check-out时的连接。
image.png
每种数据库连接池超时配置如表2-6所示。
image.png
然后开始运行测试内容,所有的数据库连接池都没有问题,但是MySQL服务器网络电缆突然故意断开,则出现了如下现象。

  • HikariCP:等待5秒,若连接未恢复,抛出SQLExceptions异常,后续getConnection()同样处理。
  • c3p0:完全无反应且无提示,也不会在CheckoutTimeout配置的时长超时后给调用者任何通知。等待2分钟后才醒来,返回一个error。
  • Tomcat:返回一个connection,若调用者如果利用这个无效的connection执行SQL语句,结果可想而知。大约55秒之后醒来,此时getConnection()返回一个error,但未像参数配置的那样等待5秒钟,而是立即返回error。
  • BoneCP:与Tomcat类似,也是约55秒后醒来,有正常反应,且最终等5秒钟后返回error。

可见,HikariCP的处理方式是最合理的。根据这个测试结果,对于各个CP处理数据库中断的情况,评分如表2-7所示。
image.png
注意 这是使用JDBC 4.1驱动程序获得的结果,结果可能因新旧驱动程序的差异而有所不同。
除了数据库中断以外,HikariCP在其他方面还有很多值得称赞的地方:

  • 连接的测试在getConnection时同步进行,并有独特的优化。
  • 在自己的事务中封装内部池的查询,含testQuery一级initSQLQuery。
  • 当连接Connections归还给连接池的时候,执行rollback操作。
  • 在Connection.close的时候,跟踪并关闭废弃语句Statements。
  • 在将Connection连接归还到客户端之前清除SQL警告。
  • 默认参数支持自动提交、事务隔离级别、catalog和只读状态。
  • 对于一些诸如“SQLException对象是否存在断开连接错误”等陷阱进行检查。

2.5 本章小结

作为一名没有什么数据库连接池基础的初学者,如何学习这门技术?如何了解它的基础和产生背景?这是很多初学者会面临的问题。
本章结合笔者自身的体会来阐述和介绍数据库连接池的产生原因、概念以及原理,进一步像百晓生点《兵器谱》一样盘点了业界主流的数据库连接池,像历史书一样介绍了从第一代数据库连接池到第二代数据库连接池过渡的这20年中的风风雨雨,并对主流的数据库连接池做了较为全面的对比总结。读完本章,相信读者可以了解到第一代数据库连接池c3p0、Proxool、Xapool分别因为什么原因像恐龙那般灭绝,也能了解到第二代数据库连接池DBCP、Tomcat JDBC Pool、BoneCP、Druid的相关概念及发展历程(DBCP经历了第一代和第二代,而Tomcat JDBC Pool就是在DBCP第一代末期产生的,BoneCP在极致性能上以25倍的高优势打败了c3p0和DBCP后,便急流勇退,将一身功力传给了HikariCP)。
阅读本章后,希望读者对于数据库连接池的功能、演进、架构等逐步产生清晰的认知,并思考一款优秀的开源产品是如何不断发展的。接下来让我们一起进入本书的核心环节——HikariCP,看看这个后起之秀Spring Boot 2.x默认的数据库连接池到底又有哪些与传统数据库连接池不一样的特色,看看它又有哪些独具匠心的设计。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接