概述
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接,来避免因为没有释放数据库连接而引起的数据库连接遗漏。
数据库连接池的原理是:在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数,最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量,使用情况等。
一款成熟商用的连接池的构成如下所示:
Springboot加载DataSource源码分析
Springboot自动加载DataSource是在DataSourceAutoConfiguration中进行的,代码如下:
// 标记这个类是一个配置类
@Configuration(proxyBeanMethods = false)
// 只有classpath下存在DataSource和EmbeddedDatabaseType这两个类才加载此配置类
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
// 开启配置属性
@EnableConfigurationProperties(DataSourceProperties.class)
// 导入DataSourcePoolMetadataProvidersConfiguration和DataSourceInitializationConfiguration两个类
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
// 定义一个内部配置类EmbeddedDatabaseConfiguration
@Configuration(proxyBeanMethods = false)
// 设置它的条件
@Conditional(EmbeddedDatabaseCondition.class)
// 缺少DataSource和XADataSource才会实例化此类
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
// 导入EmbeddedDataSourceConfiguration配置类
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
// 定义PooledDataSourceConfiguration配置类
@Configuration(proxyBeanMethods = false)
// 设置该类的加载条件PooledDataSourceCondition
@Conditional(PooledDataSourceCondition.class)
// 缺少DataSource和XADataSource才会加载此类
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
// 按照顺序会导入Hikari、Tomcat、Dbcp2、Generic和DataSourceJmxConfiguration
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
/**
* {@link AnyNestedCondition} that checks that either {@code spring.datasource.type}
* is set or {@link PooledDataSourceAvailableCondition} applies.
*/
static class PooledDataSourceCondition extends AnyNestedCondition {
PooledDataSourceCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
//
@ConditionalOnProperty(prefix = "spring.datasource", name = "type")
static class ExplicitType {
}
@Conditional(PooledDataSourceAvailableCondition.class)
static class PooledDataSourceAvailable {
}
}
/**
* {@link Condition} to test if a supported connection pool is available.
*/
static class PooledDataSourceAvailableCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("PooledDataSource");
if (DataSourceBuilder.findType(context.getClassLoader()) != null) {
return ConditionOutcome.match(message.foundExactly("supported DataSource"));
}
return ConditionOutcome.noMatch(message.didNotFind("supported DataSource").atAll());
}
}
/**
* {@link Condition} to detect when an embedded {@link DataSource} type can be used.
* If a pooled {@link DataSource} is available, it will always be preferred to an
* {@code EmbeddedDatabase}.
*/
static class EmbeddedDatabaseCondition extends SpringBootCondition {
private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource");
if (anyMatches(context, metadata, this.pooledCondition)) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
}
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
if (type == null) {
return ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll());
}
return ConditionOutcome.match(message.found("embedded database").items(type));
}
}
}
在PooledDataSourceConfiguration的@Import注解中注入了几种数据库连接池类型:
abstract class DataSourceConfiguration {
// 创建数据库连接池
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
/**
* Tomcat Pool DataSource configuration.
*/
// 创建Tomcat数据库连接池
@Configuration(proxyBeanMethods = false)
// classpatch下存在org.apache.tomcat.jdbc.pool.DataSource
@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
// 缺少DataSource类型在autoconfigure中
@ConditionalOnMissingBean(DataSource.class)
// 配置文件中存在type为org.apache.tomcat.jdbc.pool.DataSource
// 如果配置文件中指定这个值的话,这个属性是排他的,也就是只要指定了那么就会只加载这个
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource",
matchIfMissing = true)
static class Tomcat {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.tomcat")
org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) {
org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties,
org.apache.tomcat.jdbc.pool.DataSource.class);
DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl());
String validationQuery = databaseDriver.getValidationQuery();
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}
}
/**
* Hikari DataSource configuration.
*/
// 创建HikariCP数据库连接池
@Configuration(proxyBeanMethods = false)
// classpath下存在HikariDataSource
@ConditionalOnClass(HikariDataSource.class)
// 缺少DataSource类型在autoconfigure中
@ConditionalOnMissingBean(DataSource.class)
// 配置文件中存在type为com.zaxxer.hikari.HikariDataSource
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
/**
* DBCP DataSource configuration.
*/
// 创建BBCP连接池
@Configuration(proxyBeanMethods = false)
// classpath下存在BasicDataSource
@ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
// 缺少DataSource类型在autoconfigure中
@ConditionalOnMissingBean(DataSource.class)
// 配置文件中存在type为org.apache.commons.dbcp2.BasicDataSource
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource",
matchIfMissing = true)
static class Dbcp2 {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dbcp2")
org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties) {
return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class);
}
}
/**
* Generic DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {
@Bean
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
}
这里面默认会使用HikariCP的数据源。
HikariCP参数配置
校时
HikariCP在很大程度上依赖于精确的高分辨率的定时器来提供性能可可靠性,所以使用数据库连接池HikariCP的应用服务器最好能够于时间源做同步,比如NTP服务器,否则由于HikariCP源码对于时间的处理可能会导致一些问题。
HikariCP配置
(1)、必须配置
HikariCP的必须配置主要有3个,一般来说配置了这3个以后,其他默认设置在大多数系统中都表现良好且无须额外调整。这3个必须配置如下:
- dataSourceClassName或者jdbcUrl
- username,用户名
- password,密码
- dataSourceClassName和jdbcUrl是两种数据源的配置方式
HikariCP更加建议使用dataSourceClassName,当然,两者都可以接受。需要注意的是,如果是Springboot自动装配的用户,需要使用jdbcUrl的基于配置的方式;当前一直的Mysql DataSource并不支持网络超时,建议改用jdbcUrl的方式。
dataSourceClassName意思是JDBC驱动提供的DataSource的名字。用dataSourceClassName的方式。
默认值无 - jdbcUrl
jdbcUrl属性表示HikariCP使用的是传统的、基于驱动管理器DriverManager的配置,虽然HikariCP作者认为两种配置方式中,基于dataSourceClassName的配置由于各种原因而更优越,但对于许多部署而言,两者几乎没有显著差异。
默认值无 - username和password
username和password分别表示从基础驱动程序获取Connections时使用的默认身份验证用户名和密码。
注意,对于DataSource,它通过调用DataSource.getConnection(username, password)来操作底层DataSource,从而以一种非常确定的方式工作。但是,对于基于驱动程序的配置,每个驱动程序Driver都不同。在这种情况下,HikariCP会将username属性和password属性分别配置在Properties文件中,从而传递给Driver的DriverManager.getConnection(jdbcUrl, props)调用。也可以直接跳过此方法,并调用addDataSourceProperty("username", ...)或者addDataSourceProperty("pass", ...)。
默认值无 - HikariCP初始化配置示例:HikariCP采取的是基于DataSource的配置方式,可读性很高。dataSourceClassName或者jdbcUrl,以及username用户名和password密码都是HikariCP初始化的必须配置。
第一种方式是HikariConfig。通过HikariConfig设置jdbcUrl、username、password等核心配置。其他使用addDataSourceProperty集成进来。示例代码如下:
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://*********:3306/mybatis");
hikariConfig.setUsername("****");
hikariConfig.setPassword("********");
hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
对于这种方式可以扩展一下,如果需要启用SSL,也可以使用addDataSourceProperty完成。代码如下:
hikariConfig.addDataSourceProperty("ssl", "true");
第2种方式是直接实例化一个HikariDataSource。这种方式主要是为了方便Spring和其他IOC配置框架而提供的。和第1种方式构造函数导致赋值立即发生不一样,第2种方式是在运行getConnection时发生赋值的。赋值的目的就是在启动数据库连接池以后,使用户不需要使用配置再次修改数据库连接池的参数。修改数据库连接池唯一支持的方法就是通过HikariConfigMXBean,但是前提是需要开启HikariConfigMXBean的配置,它和第一种方法不能合并混用,假如使用的是HikariConfig方法,那么就不要在HikariDataSource方法上调用setRegisterMBean(true)了,而是要在HikariConfig对象中设置并创建HikariDataSource之前执行。
第三种方式是加载外部property属性文件,示例代码如下
HikariConfig hikariConfig = new HikariConfig("hikari.properties");
HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
dataSource.user=test
dataSource.password=test
dataSource.databaseName=mydb
dataSource.portNumber=5432
dataSource.serverName=localhost
第四种方式是使用java.util.Properties,示例代码如下:
Properties properties = new Properties();
properties.setProperty("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource");
properties.setProperty("dataSource.user", "test");
properties.setProperty("dataSource.password", "test");
properties.setProperty("dataSource.databaseName", "mydb");
properties.setProperty("dataSource.portNumber", "5432");
properties.setProperty("dataSource.serverName", "localhost");
HikariConfig hikariConfig = new HikariConfig(properties);
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
非必须配置
- 常用配置
常用配置有10个。
(1)autoCommit:此属性控制从池返回的连接的默认自动提交行为。它是一个布尔值,默认值:true。
(2)connectionTimeout:此属性控制客户端(即用户的程序)等待池中连接的最长毫秒数。如果在没有连接可用的情况下超过此时间,则将抛出SQLException异常。最低可接受的连接超时为250毫秒。默认值:30000(30秒)。这是一个很重要的问题排查指标。
(3)idleTimeout:此属性控制连接允许被闲置在池中的最大时间。此设置仅适用于minimumIdle定义为比maximumPoolSize小的时候。一旦池到达minimumIdle连接的时候,空闲连接将不会退役。连接是否空闲而退役的最大变化为+30秒、平均变化为+15秒。在此超时之前,连接永远不会因空闲状态而退役。值为0意味着空闲连接永远不会从池中删除即永不超时。minimum允许的最小值为10000毫秒。默认值:600000.
在繁忙的数据库连接池种,连接可能永远不会达到idleTimeout。但是,许多防火墙和负载均衡器(通常位于应用程序和数据库之间)会占用套接字生存周期。通常,达到IdleTimeout时,无论当前流量如何,都会切断连接。
HikariCP设计之初就不支持空闲连接检测test-while-idle,这是因为数据库管理员DBA往往会默认设置数据库最长连接时间是60秒,test-while-idle会对数据库产生不必要的查询,这样就有可能导致数据库空闲连接出现超时的问题。在HikariCP的旧版本种,maxLifetime由管家线程HouseKeeper强制执行,每30秒执行一次,因为wait_timeout减去30秒就是推荐的maxLifetime。但是最新版本的HikariCP对每个连接connection进行专用计时器任务,提供了几十毫秒(在高负载下长达几秒)的时间间隔,此时maxLifetime被安全地设置为wait_timeout减去5秒。如果连接退出,后台线程会执行添加操作,创建新的连接大约是5毫秒。如果maxLifetime是60秒,那么idleTimeout可以被设置为0。
(4)maxLifetime:maxLifetime属性用来控制池中连接的最大生命周期。使用中的连接永远不会退役,只有关闭后连接才会被移除。HikariCP不会让所有的连接同时退役,而是巧妙地对每一个连接都设置了轻微的负衰减值,以避免池中的连接大规模消亡。HikariCP作者强烈建议用户设置此值,并且它应比任何数据库或者基础设施实施的连接时间限制短几秒。值0表示没有最大生存期(无限生存期),当然取决于idleTimeout设置。默认值为1800000(30分钟)。
(5)connectionTestQuery:如果驱动程序支持JDBC4,则建议不要设置此属性。这适用于不支持JDBC4的Connection.isValid()的遗留驱动程序API。这是一个检测查询,在数据库连接池给出连接之前进行查询,以验证与数据库的连接是否仍然存在且有效。同样,尝试运行数据库连接池不配置该属性,如果你的驱动程序不支持JDBC4,那么HikariCP将报错。该属性默认值:无。
(6)minimumIdle:minimumIdle大致相当于BoneCP中的minConnectionPerPartition参数。此属性控制HikariCP尝试在池中维护的最小空闲连接数。若空闲连接数低于此值且池中的总连接数小于maximumPoolSize,则HikariCP将尽最大努力快速地添加其他连接。然而,为了最大限度地提高性能和对峰值需求的响应能力,HikariCP的作者建议不要设置此值,而是允许HikariCP充当一个固定大小的连接池,因此即使idleTimeout设置为1分钟,一旦连接关闭,它也会在池中进行调解控制,即使使用情况上下浮动,HikariCP也会保持minimumIdle连接可用。minimumIdle的默认值与maximumPoolSize相同。
minimumIdle应始终小于或等于maximumPoolSize。如果minimumIdle设置为更高的值,maximumPoolSize将被推高到相等的值。minimumIdle逻辑上不能超过maximumPoolSize,因为maximumPoolSize指定了后端数据库的实际连接的最大数量。
如果有比minimumIdle的数目更多的连接数,此时若一个连接退役了,则它不会被自动替换。但是如果数据库连接池被配置为固定大小,或者连接关闭后空闲连接小于minimumIdle的数量,那么就会立即自动替换被关闭的连接。启用HikariCP的metrics采集,在可视化界面上可直观地显示连接的直方图,这有利于用户研究并确定正确、合理的minimumIdle值即idleTimeout值。数据库连接池的调优最好基于经验数据。
(7)maximumPoolSize:该属性控制数据库连接池连接数允许到达的最大值,包括空闲和正在使用的连接。基本上,此值将决定数据库后端实际连接的最大数量。该属性的合理值最好由用户的执行环境决定。当池达到大小且没有空闲连接可用时,对getConnection()的调用将阻塞到超时前connectionTimeout毫秒。
(8)metricRegistry:该属性仅在编程配置或IOC容器中可用。该属性允许用户指定池使用的Codehale/Dropwizard实例MetricRegistry,来记录各种度量标准。
(9)poolName:该属性表示连接池的用户定义名称,主要显示在日志记录和JMX管理控制台中,以标识池和池配置。该属性的默认值是自动生成的。
HikariCP连接池配置大小思路
在拥有一个CPU内核的计算机可以同时执行数十或数百个线程,其实这只是操作系统的一个time-slcing(时间切片)的效果。实际上,单核一次只能执行一个线程,然后由操作系统切换上下文,内核开始为另一个线程执行代码,依此类推。这是一个基本的计算法则,给定一个CPU资源,按顺序执行A和B总是比通过时间片同时执行A和B要快。一旦线程数量超过了CPU核心的数量,再添加更多的线程就会变慢,而不是更快。设计多线程是为了尽可能地利用CPU空闲等待时间,它的代价就是要增加部分CPU时间来实现线程切换。如果CPU空闲等待时间已经比线程切换所用时间更短(线程越多,切换消耗越大),那么线程切换会非常影响性能,成为系统瓶颈。
HikariCP与JDBC
JDBC API是一种Java API,用于访问几乎任何类型的表格数据。JDBC API由一组用Java编写的类和接口组成,为工具/数据库开发人员提供标准API的编程语言,可以使用全Java API编写的工业级数据库应用程序。
需要强调的是,Connection、Statement和Result是一种爷-父-子关系,对Connection的管理,就是对数据库资源的管理。如果想确定某个数据库连接(Connection)是否超时,则需要确定其(所有的)子Statement是否超时,同样需要确定所有相关的ResultSet是否超时;Statement关闭会导致ResultSet关闭,但是Connection关闭却不一定会导致Statement关闭。在数据库连接池里,Connection关闭并不是物理关闭,只是归还连接池,所以Statement和ResultSet有可能被持有,并且实际占用相关的数据库的游标资源。所以在关闭Connection前,需要关闭所有相关的Statement和ResultSet。这就是HikariCP作者所强调的JDBC最基本的规范。
最好的方案就是顺序关闭ResultSet、Statement、Connection;rs.close()和stmt.close()后面加上rs=null和stmt=null来防止内存泄漏;RowSet不依赖于Connection和Statement,可以作为一种传递ResultSet的替代方案。
通过DriverManager获得Connection,一个Connection对应一个实际的物理连接,每次操作都需要打开物理连接,使用完后立即关闭。频繁打开/关闭连接都会造成不必要的数据库性能消耗。在这样的背景下催生了数据库连接池,并将这些连接组织成连接池,每次请求连接时,无须重新打开连接,而是从池中取出已有连接,使用完后并不实际关闭连接,而是将其归还给池。所以这里涉及两项技术,一是连接使用List之类的集合进行初始化、装载和归还,二是使用动态代理来把资源归还给List集合。HikariCP之所以这么快,主要是因为这两项技术做到了极致。
JDBC数据库连接池使用javax.sql.DataSource表示,DataSource只是一个接口,其实现通常由服务提供商。
PreparedStatement和Statement
在企业开发中被强烈推荐使用PreparedStatement,原因主要有以下方面:
(1)、Statement会频繁编译SQL。如果JDBC驱动支持的话,PreparedStatement可对SQL进行预编译,提高效率,预编译的SQL存储在PreparedStatement对象中。从这个意义上来说,PreparedStatement比Statement更快,使用PreparedStatement也可以降低生产环境的数据库负载。
(2)、Statement对象编译SQL语句时,如果SQL语句有变量,就需要使用分隔符来隔开,如果变量非常多,就会使SQL变得非常复杂。PreparedStatement可以使用占位符,通过动态参数化的查询来简化SQL的编写。
(3)、PreparedStatement可防止SQL注入。因为Statement对象需要拼接,通过分隔符++等恒等式就可以实现SQL注入;而PreparedStatement使用占位符,就不会有SQL注入的问题。
JDBC与SPI
面向对象的设计里,我们一直推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可插拔的原则,如果需要替换一种实现,就需要修改代码。于是就有了SPI服务发现机制。
在Java中根据一个子类获取其父类或接口信息非常方便,但是根据一个接口获取该接口的所有实现类却没那么容易。有一种办法就是扫描classpath下所有的class与jar包中的class,接着用ClassLoader加载进来,再判断是否是给定接口的子类。但是这种方法的代价实在太大,一般不会使用。
根据这个问题,Java推出了ServiceLoader类来提供服务发现机制,动态地为某个接口寻找服务实现。当服务的提供者提供了服务接口的一种实现之后,必须根据SPI约定在META-INF/services目录里创建一个以服务接口命名的文件,该文件里写的就是实现该服务接口的实现类。当程序调用ServiceLoader的load方法的时候,ServiceLoader能够通过约定的目录找到指定的文件,并装载实例化,完成服务发现。
举个例子:
com.example.matrixweb.spi.apple.Apple
com.example.matrixweb.spi.pear.Pear
测试类代码如下:
ServiceLoader<Fruit> loader = ServiceLoader.load(Fruit.class);
Iterator<Fruit> iterable = loader.iterator();
while (iterable.hasNext()) {
System.out.println(iterable.next().getName());
}
注意:ServiceLoader是JDK6里面引进的一个特性,它主要用来装载一系列的service provider可以通过service provider的配置文件来装载指定的service provider。这里针对ServiceLoader还有一个特定的限制,就是我们提供的这些具体实现类必须提供无参数的构造函数,否则ServiceLoader就会报错。ServiceLoader是一个可以实现动态加载具体实现类的机制,通过它可以实现代码的解耦,也可以实现类似于IOC的效果
线程池技术
池化技术,包括线程池、连接池、内存池、对象池等,其作用就是提前保存大量的资源,或者将用过的资源保存起来,等下一次需要时再取出来重复使用。
线程池和连接池是两个不同的概念,连接池一般在客户端设置,而线程池是在如数据库这样的服务器端配置。通常来说,比较好的方式是将连接池和线程池结合起来使用。线程池具有线程复用、控制最大并发数、管理线程、保护系统4个优点,线程池的目的类似与连接池,通过复用线程来减少频繁创建和销毁线程。
线程池往往配合队列一起工作,限制并发处理任务的数量,从而保护系统。线程池的工作方式大同小异,先将任务提交到一个或者多个队列中,接着一定数量的线程从该队列中领取任务并执行,任务的结果再做处理,比如保存到Mysql数据库,调用别的服务等;任务完成以后,该线程会返回任务队列,等待下一个任务。
和HikariCP的配置比较类似,线程池一般需要配置核心线程池大小和线程池最大值。除了核心线程池的线程一直存在以外,外部的线程空闲一段时间之后会被回收。线程池配置得太大可能会造成僵死、响应慢等问题,配置太小又会造成创建线程成本高昂等问题。线程池在生产中的配置通常根据CPU密集还是IO密集,以及机器配置、压测、业务场景等来做决定。
数据库连接池虽然避免了连接频繁创建和销毁的情况,但是却无法达到控制Mysql活动线程数的目标。在高并发场景下,随着数据库访问量的增大,数据库的响应时间也会随之越来越大,数据库的吞吐量也会越来越大,性能也会表现不佳,所以数据库连接池并不一定能够起到保护数据库的作用,还可能产生数据库的雪崩。因此,Mysql线程池是对数据库连接池的一个强有力的补充。
HikariCP性能揭秘
HikariCP所做的优化,总结如下:
- 优化并精简字节码、优化代码和拦截器。
- 使用FastList替代ArrayList。
- 有更好的并发集合类实现ConcurrentBag。
- 其他针对BoneCP缺陷的优化。
精简字节码
HikariCP利用了一个第三方的Java字节码修改类库Javassist来生成委托实现动态代理。
动态代理的实现在com.zaxxer.hikari.pool.ProxyFactory类中,代码如下:
public final class ProxyFactory
{
private ProxyFactory()
{
// unconstructable
}
/**
* Create a proxy for the specified {@link Connection} instance.
* @param poolEntry the PoolEntry holding pool state
* @param connection the raw database Connection
* @param openStatements a reusable list to track open Statement instances
* @param leakTask the ProxyLeakTask for this connection
* @param now the current timestamp
* @param isReadOnly the default readOnly state of the connection
* @param isAutoCommit the default autoCommit state of the connection
* @return a proxy that wraps the specified {@link Connection}
*/
static ProxyConnection getProxyConnection(final PoolEntry poolEntry, final Connection connection, final FastList<Statement> openStatements, final ProxyLeakTask leakTask, final long now, final boolean isReadOnly, final boolean isAutoCommit)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
static Statement getProxyStatement(final ProxyConnection connection, final Statement statement)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
static CallableStatement getProxyCallableStatement(final ProxyConnection connection, final CallableStatement statement)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
static PreparedStatement getProxyPreparedStatement(final ProxyConnection connection, final PreparedStatement statement)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
static ResultSet getProxyResultSet(final ProxyConnection connection, final ProxyStatement statement, final ResultSet resultSet)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
static DatabaseMetaData getProxyDatabaseMetaData(final ProxyConnection connection, final DatabaseMetaData metaData)
{
// Body is replaced (injected) by JavassistProxyFactory
throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run.");
}
}
这些代码基本代理了JDBC常用的核心接口,一共是5个,分别为ProxyConnection、Statement、CallableStatement、PreparedStatement、ResultSet,并且每个方法都抛出了异常。其实每个方法在抛异常之前都有一段body,这段body是在编译时调用JavassistProxyFactory才生成的。
JavassistProxyFactory存在于com.zaxxer.hikari.util包中,是Javassist的工具包,它主要有两个核心方法:generateProxyClass方法负责生成实际使用的代理类字节码,modifyProxyFactory对应修改工厂类中的代理类获取方法Proxy*.java为HikariProxy*.java。这个工具包的作用是将ProxyConnection、ProxyStatement、ProxyPreparedStatement、ProxyCallableStatement、ProxyResultSet这5个com.zaxxer.hikari.pool包下代理类,利用Javassist重构后生成实际的HikariCP的对应代理类HikariProxyConnection、HikariProxyStatement、HikariProxyPreparedStatement、HikariProxyCallableStatement、HikariProxyResultSet。之所以使用Javassist生成动态代理,是因为其速度快,比JDK Proxy生成的字节码更少,精简了很多不必要的字节码。
此外,HikariCP在字节码工程中还对JIT进行了优化。比如JIT方法内联优化默认的字节码个数阈值是35字节,低于35字节才会进行优化。HikariCP在精简字节码的时候,研究了编译器的字节码输出,甚至是JIT的汇编输出,以将关键部分限制为小于JIT内联阈值,展平了继承层次结构,隐藏成员变量,消除了强制转换。
FastList
HikariCP一个性能方面的出彩的优化方案就是FastList。
- 当调用Connection.prepareStatement()的时候,新的PreparedStatement就被添加到FastList。
- 当调用PreparedStatement.close()的时候,这个statement就从FastList中被移除的。
- 调用Connection.close()的时候,任何未明确关闭的语句都将从FastList移除并关闭。
但是HikariCP并没有拦截PreparedStatement.addBatch()方法,所以实际上addBatch()不可能添加任何内容到FastList。executeBatch方法既不会消除批处理,也不会将PreparedStatement从FastList中移除。唯一能够清除批处理的是PreparedStatement.clearBatch()方法,而唯一能够从FastList中移除PreparedStatement的方法就是调用PreparedStatement.close()或者Connection.close()方法。如果使用Java8,强烈推荐使用try-with-resources语法,让Java自己清理资源,使代码更清晰简洁。
DataSource dataSource = null;
List<User> users = new ArrayList<>();
User user = new User();
user.setId(12);
user.setPassword("123");
users.add(user);
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("update user set password=? where id=?")) {
int batchCount = 0;
for (User tmp : users) {
statement.setString(1, tmp.getPassword());
statement.setLong(2, tmp.getId());
statement.addBatch();
if (++batchCount == 100) {
statement.executeBatch();
statement.clearBatch();
batchCount = 0;
}
}
if (batchCount > 0) {
statement.executeBatch();
statement.clearBatch();
}
connection.commit();
} catch (SQLException e) {
log.error("add batch error.", e);
}
如上述所示,对于批处理语句执行清理的过程分为如下几步:DataSource.getConnection()、Connection.prepareStatement()、多次调用PreparedStatement.addBatch()、调用PreparedStatement.executeBatch()/clearBatch()、依赖Java8的try-with-resources语法进行资源清理。
FastList是一个List接口的精简实现,只实现了接口中必要的几个方法。JDK ArrayList每次调用get()方法时都会进行rangeCheck,检查索引是否越界,FastList的实现中去除了这一检查,只要保证索引合法那么rangeCheck就成为不必要的计算开销(当然开销极小)。此外,HikariCP使用List来保存打开的Statement,当Statement关闭或Connection关闭时需要将对应的Statement从List中移除。通常情况下,JDBC在同一个Connection创建了多个Statement时,后打开的Statement会先关闭。这种情况从尾部开始扫描将表现更好。ArrayList的remove(Object)方法是从头开始遍历数组,而FastList是从数组的尾部开始遍历,因此更为高效,它消除了范围检查,并从尾部到头部执行移除扫描。简而言之就是用自定义数组类型(FastList)代替ArrayList,避免每次get()调用都要进行范围检查,避免调用remove()时的从头到尾的扫描。
两者代码比较如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
// 后者与前者相比并没有继承AbstractList。
public final class FastList<T> implements List<T>, RandomAccess, Serializable
// ArrayList的get方法,可以看到,每次get的时候都会进行rangeCheck。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 而com.zaxxer.hikari.util.FastList的get方法则取消了rangeCheck,在一定程度上追求了极致。
@Override
public T get(int index)
{
return elementData[index];
}
// 再看ArrayList的remove(Object)方法,可以看到,它从头开始遍历数组进行移除。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 而FastList的remove(Object)方法,从数组的尾部开始遍历,因而更加高效。
@Override
public boolean remove(Object element)
{
for (int index = size - 1; index >= 0; index--) {
if (element == elementData[index]) {
final int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return true;
}
}
return false;
}
ConcurrentBag
ConCurrentBag的名字来源于C#.NET的同名类,但是实现却不一样。它是一个lock-free集合,在连接池(多线程数据交互)的实现上具有比LinkedBlockingQueue和LinkedTransferQueue更优越的并发读写性能。它具有无锁设计、ThreadLocal缓存、队列窃取、直接切换优化四大特点。
ConcurrentBag采用了queue-stealing的机制获取元素:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则再次从共享的CopyOnWriteArrayList中获取。此外,ThreadLocal和CopyOnWriteArrayList在ConcurrentBag中都是成员变量,线程间不共享,避免了伪共享的发生。
ConcurrentBag的性能提升主要源于如下3个组成部分。
- CopyOnWriteArrayList:负责存放ConcurrentBag中全部用于出借的资源。
- ThreadLocal:用于加速线程本地化资源访问。
- SynchronousQueue:用于存在资源等待线程时的第一手资源交接。
ConcurrentBag源码解析
ConcurrentBag内部同时使用ThreadLocal和CopyOnWriteArrayList来存储元素,其中CopyOnWriteArrayList是线程共享的。ConcurrentBag采用了queue-stealing的机制获取元素;首先尝试从ThreadLocal中获取属于当前线程的元素避免锁竞争,如果没有可用元素则扫描公共集合,再从共享的CopyOnWriteArrayList中获取(ThreadLocal列表中没有被使用的items在借用的线程还没有属于自己的时候,是可以被窃取的)。
ThreadLocal和CopyOnWriteArrayList在ConcurrentBag中都是成员变量,线程间不共享,避免了伪共享的发生。
其使用专门的AbstractQueuedLongSynchronizer来管理跨线程信号,这是一个lock-less的实现。
这里需要特别注意的是,ConcurrentBag通过borrow方法进行数据资源借用,通过requite方法进行数据资源回收,注意其中borrow方法只提供对象引用,不移除对象。所以从bag中借用的items实际上并没有从任何集合中删除,因此即使引用废弃了,垃圾收集也不会发生。因此使用引用时通过borrow取出的对象必须通过requite方法进行放回,否则会导致内存泄漏。只有remove方法才能完全从bag中删除一个对象。
CopyOnWriteArrayList负责存放ConcurrentBag中全部用于出借的资源,sharedList中的资源通过add方法添加,用remove方法会出借。
SynchronousQueue
SynchronousQueue来自于JUC并发包java.util.concurrent,在HikariCP中的体现就是ConcurrentBag结构中的handoffQueue,它主要用于存在资源等待线程时的第一手资源交接。
SynchronousQueue提供了以下两个构造函数:
/**
* Creates a {@code SynchronousQueue} with nonfair access policy.
*/
public SynchronousQueue() {
this(false);
}
/**
* Creates a {@code SynchronousQueue} with the specified fairness policy.
*
* @param fair if true, waiting threads contend in FIFO order for
* access; otherwise the order is unspecified.
*/
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
在HikariCP中,选择的是公平模式this.handoffQueue = new SynchronousQueue<>(true)。公平模式总结下来就是:队尾匹配对头出队,先进先出,体现公平原则。
SynchronousQueue是一个无存储空间的阻塞队列(是实现newFixedThreadPool的核心),非常适合做交换工作,生产者和消费者的线程同步以传递某些信息、事件或者任务。作为BlockingQueue中的一员,SynchronousQueue的吞吐量高于ArrayBlockingQueue和LinkedBlockingQueue,与其他BlockingQueue有着不同特性。
- SynchronousQueue无存储空间。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。它的特点是每一个put操作必须要等待一个take操作或者poll方法,才能使用off、add方法,否则不能继续添加元素,反之亦然。
- 因为没有容量,所以对应peek、contains、clear、isEmpty等方法其实是无效的。
- SynchronousQueue分为公平和不公平两种情况,默认情况下采用不公平的访问策略。当然也可以通过构造函数来将其设置为公平访问策略。
- 若使用TransferQueue,则队列中永远会存在一个dummy node。
代码解析注释
//可用连接同步器, 用于线程间空闲连接数的通知, synchronizer.currentSequence()方法可以获取当前数量
//其实就是一个计数器, 连接池中创建了一个连接或者还回了一个连接就 + 1, 但是连接池的连接被借走, 是不会 -1 的, 只加不减
//用于在线程从连接池中获取连接时, 查询是否有空闲连接添加到连接池, 详见borrow方法
private final QueuedSequenceSynchronizer synchronizer;
//sharedList保存了所有的连接
private final CopyOnWriteArrayList<T> sharedList;
//threadList可能会保存sharedList中连接的引用
private final ThreadLocal<List<Object>> threadList;
//对HikariPool的引用, 用于请求创建新连接
private final IBagStateListener listener;
//当前等待获取连接的线程数
private final AtomicInteger waiters;
//标记连接池是否关闭的状态
private volatile boolean closed;
/**
* 该方法会从连接池中获取连接, 如果没有连接可用, 会一直等待timeout超时
*
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return a borrowed instance from the bag or null if a timeout occurs
* @throws InterruptedException if interrupted while waiting
*/
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
//①
//先尝试从ThreadLocal中获取
List<Object> list = threadList.get();
if (weakThreadLocals && list == null) {
//如果ThreadLocal是 null, 就初始化, 防止后面 npe
list = new ArrayList<>(16);
threadList.set(list);
}
//②
//如果ThreadLocal中有连接的话, 就遍历, 尝试获取
//从后往前反向遍历是有好处的, 因为最后一次使用的连接, 空闲的可能性比较大, 之前的连接可能会被其他线程偷窃走了
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
@SuppressWarnings("unchecked") final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
//③
//如果没有从ThreadLocal中获取到连接, 那么就sharedList连接池中遍历, 获取连接, timeout时间后超时
//因为ThreadLocal中保存的连接是当前线程使用过的, 才会在ThreadLocal中保留引用, 连接池中可能还有其他空闲的连接, 所以要遍历连接池
//看一下requite(final T bagEntry)方法的实现, 还回去的连接放到了ThreadLocal中
timeout = timeUnit.toNanos(timeout);
Future<Boolean> addItemFuture = null;
//记录从连接池获取连接的开始时间, 后面用
final long startScan = System.nanoTime();
final long originTimeout = timeout;
long startSeq;
//将等待连接的线程计数器加 1
waiters.incrementAndGet();
try {
do {
// scan the shared list
do {
//④
//当前连接池中的连接数, 在连接池中添加新连接的时候, 该值会增加
startSeq = synchronizer.currentSequence();
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
// if we might have stolen another thread's new connection, restart the add...
//⑤
//如果waiters大于 1, 说明除了当前线程之外, 还有其他线程在等待空闲连接
//这里, 当前线程的addItemFuture是 null, 说明自己没有请求创建新连接, 但是拿到了连接, 这就说明是拿到了其他线程请求创建的连接, 这就是所谓的偷窃了其他线程的连接, 然后当前线程请求创建一个新连接, 补偿给其他线程
if (waiters.get() > 1 && addItemFuture == null) {
//提交一个异步添加新连接的任务
listener.addBagItem();
}
return bagEntry;
}
}
} while (startSeq < synchronizer.currentSequence()); //如果连接池中的空闲连接数量比循环之前多了, 说明有新连接加入, 继续循环获取
//⑥
//循环完一遍连接池(也可能循环多次, 如果正好在第一次循环完连接池后有新连接加入, 那么会继续循环), 还是没有能拿到空闲连接, 就请求创建新的连接
if (addItemFuture == null || addItemFuture.isDone()) {
addItemFuture = listener.addBagItem();
}
//计算 剩余的超时时间 = 用户设置的connectionTimeout - (系统当前时间 - 开始获取连接的时间_代码①处 即从连接池中获取连接一共使用的时间)
timeout = originTimeout - (System.nanoTime() - startScan);
} while (timeout > 10_000L && synchronizer.waitUntilSequenceExceeded(startSeq, timeout)); //③
//⑦
//这里的循环条件比较复杂
//1. 如果剩余的超时时间, 大于10_000纳秒
//2. startSeq的数量, 即空闲连接数超过循环之前的数量
//3. 没有超过超时时间timeout
//满足以上 3 个条件才会继续循环, 否则阻塞线程, 直到满足以上条件
//如果一直等到timeout超时时间用完都没有满足条件, 结束阻塞, 往下走
//有可能会动态改变的条件, 只有startSeq数量改变, 是②处添加的创建连接请求
} finally {
waiters.decrementAndGet();
}
return null;
}
/**
* 该方法将借出去的连接还回到连接池中
* 不通过该方法还回的连接会造成内存泄露
*
* @param bagEntry the value to return to the bag
* @throws NullPointerException if value is null
* @throws IllegalStateException if the requited value was not borrowed from the bag
*/
public void requite(final T bagEntry) {
//⑧
//lazySet方法不能保证连接会立刻被设置成可用状态, 这是个延迟方法
//这是一种优化, 如果要立即生效的话, 可能会需要使用volatile等, 让其他线程立即发现, 这会降低性能, 使用lazySet浪费不了多少时间, 但是不会浪费性能
bagEntry.lazySet(STATE_NOT_IN_USE);
//⑨
//将连接放回到threadLocal中
final List<Object> threadLocalList = threadList.get();
if (threadLocalList != null) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
//通知等待线程, 有可用连接
synchronizer.signal();
}
/**
* 在连接池中添加一个连接
* 新连接都是添加到sharedList中, threadList是sharedList中的部分连接的引用
*
* @param bagEntry an object to add to the bag
*/
public void add(final T bagEntry) {
if (closed) {
LOGGER.info("ConcurrentBag has been closed, ignoring add()");
throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
}
//⑩
sharedList.add(bagEntry);
synchronizer.signal();
}
/**
* 从连接池中移除一个连接.
* 这个方法只能用于从<code>borrow(long, TimeUnit)</code> 或者 <code>reserve(T)</code>方法中获取到的连接
* 也就是说, 这个方法只能移除处于使用中和保留状态的连接
*
* @param bagEntry the value to remove
* @return true if the entry was removed, false otherwise
* @throws IllegalStateException if an attempt is made to remove an object
* from the bag that was not borrowed or reserved first
*/
public boolean remove(final T bagEntry) {
//⑪
//尝试标记移除使用中和保留状态的连接, 如果标记失败, 就是空闲的连接, 直接返回 false
//也就是检查连接的状态, 不能移除空闲的连接或者已经标记移除的连接
if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
return false;
}
//如果上面标记成功了, 那么从连接池中移除这个连接
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
// synchronizer.signal();
return removed;
}
补充
borrow方法应该是整个 HikariCP 中最最核心的方法,它是我们从连接池中获取连接的时候最终会调用到的方法,一切秘密都在这里了。我们分析下:
①ThreadLocal
//①
//先尝试从ThreadLocal中获取
List<Object> list = threadList.get();
if (weakThreadLocals && list == null) {
//如果ThreadLocal是 null, 就初始化, 防止后面 npe
list = new ArrayList<>(16);
threadList.set(list);
}
threadList是在ConcurrentBag上的成员变量,它的定义是private final ThreadLocal threadList;,可见它是一个ThreadLocal,也就是每个线程都有独享的一个 List,它是用于保存当前线程用过的连接。注意这里我说的是"用过",不是所有的连接。因为还有一个成员变量private final CopyOnWriteArrayList
HikariCP 使用过的连接,在还回连接池的时候,是直接放在了ThreadLocal中。说到这里,可能会有同学问了:sharedList中保存了所有的连接,当用户借走了一个连接,不是应该把这个连接从sharedList中移除,然后还回来的时候再把连接加入到sharedList中?为什么还回去的时候,没有放到sharedList中呢?
首先,明确一点,HikariCP 不是这样做的。为什么呢?如果用户借用连接的时候,你从sharedList中移除了,那么相当于这个连接脱离了 HikariCP 的管理,后面 HikariCP 还怎么管理这个连接呢?比如这个连接的生命周期到时间了,连接都让用户拐跑了,我还怎么关闭这个连接呢?所以,所有的连接都不能脱离掌控,一个都不能少。其实,我们在sharedList中保存的仅仅是数据库连接的引用,这些连接是所有的线程都可见的,各个线程也可以随意保存连接的引用,只是要使用的时候必须要走borrow方法,按流程来。
为什么要放到线程的threadList中?
因为下次获取的时候比较方便,也许会提高性能。每个线程都优先从自己的本地线程中拿,竞争的可能性大大降低啊,也许这个连接刚刚用完到再次获取的时间极短,这个连接很可能还空闲着。只有在本地线程中的连接都不能使用的时候,才去sharedList这个 HikariCP的总仓库里获取。
举一个生活例子:假如你是一个连锁店老板,提供汽车出租服务,有一个总仓库,所有的连锁店都从这里提车出租给用户。刚开始,你是每租一辆车都去仓库直接提货,用户还车的时候,你直接送到仓库。过了一段时间,你觉得这样不行啊,太浪费时间了,而且所有的连锁店都这样,各个店的老板都去提车,太忙了,还得排队。要不用户还回来的车先放店里吧,这样下次有用户租车就不用去仓库了,直接给他,方便很多,店里没车了再去总仓提车。其他连锁店都开始这么搞,大家都先用店里的车不够再去总仓。生意火爆,有一天店里没车了,你去仓库提车,仓库管理员说:仓库也没车了,天通苑的连锁店里有闲着的,你去那里提吧,于是你把天通苑连锁店的车借走了。所以各个连锁店之间也有相互借车。
例子可能不太恰当,一时也想不到同样道理的生活例子,但是就这个意思。HikariCP 也是这样,用户使用的连接,还回连接池的时候,直接放到线程的本地threadList中,如果用户又要借用连接,先看本地有没有,优先使用本地连接,只有本地没有或者都不可用的时候,再去 HikariCP 的连接池里获取。但是跟借车不同,因为我们本地是保存的sharedList中连接的引用,虽然你还有这个连接的引用,但是很可能它已经被其他线程从sharedList借走了,这就是HikariCP所谓的线程间的连接窃取。所以线程在本地的threadList就算拿到了连接,也必须检查下状态,是不是可用的。
说到这里,还没有解析代码,扯远了。①出代码就是先从本地的threadList里取出连接的 List,然后检查下List 是否为空,是空的直接初始化一个 List,因为下面要用到,防止抛空指针了。大家可以看到判空的时候,还有一个条件是weakThreadLocals,这个标识是表示threadList是否是弱引用。如果是弱引用,那么很可能 GC 的时候会被回收掉,所以变成 null 了,但是如果不是弱引用的话,那么它是在初始化ConcurrentBag的时候,就是一个FastList了,不用担心是 null。那么什么情况下threadList会是弱引用呢?当 HikariCP 运行在容器中时,会使用弱引用,因为在容器重新部署的时候,可能会导致发成内存泄露,具体大家可以看下#39 的 issue。
HikariCP连接原理
数据库连接池负责分配、管理和释放数据库连接。它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接,来避免因为没有释放数据库连接而引起的数据连接遗漏。核心功能主要包括连接的生成、获取、归还、销毁等生命周期。
获取连接
HikariCP可以被理解为一个简单的BlockingQueue,但它实际上并没有使用BlockingQueue,而是使用了一种被称为ConcurrentBag的专门的集合。
HikariCP确实有几个自己的线程:有一个HouseKeeper管家线程,定期运行以退出空闲连接;一个调度线程,可以退出到达maxLifetime的连接;一个用于添加连接的线程;一个用于关闭上述线程的线程等。接下来,我们从获取连接部分开始仔细研究源码级的原理。
获取连接是HikariCP的核心功能,HikariCP对象首先通过getConnection方法获得HikariPool真正的getConnection方法,HikariPool内部通过ConcurrentBag(一个lock-free集合,在连接池及多线程交互的实现上具有比LinkedBlockingQueue和LinkedTransferQueue更优越的并发读写性能)的borrow方法获取PoolEntry,它将首先尝试从线程的ThreadLocal最近使用的连接列表中获取未使用的连接。最后通过PoolEntry执行createProxyConnection方法创建一个物理连接并返回其代理链接ProxyConnection。
顾名思义,HikariDataSource就是HikariCP对外提供给用户的定制化的DataSource,使用Spring的用户可以直接将它作为数据源。用户可以按如下方法初始化HikariDataSource。
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/simple");
dataSource.setUsername("root");
dataSource.setPassword("123");
HikariDataSource实现了java.io.Closeable接口,它继承了java.lang.AutoCloseable接口。
public interface Closeable extends AutoCloseable {
/**
* Closes this stream and releases any system resources associated
* with it. If the stream is already closed then invoking this
* method has no effect.
*
* <p> As noted in {@link AutoCloseable#close()}, cases where the
* close may fail require careful attention. It is strongly advised
* to relinquish the underlying resources and to internally
* <em>mark</em> the {@code Closeable} as closed, prior to throwing
* the {@code IOException}.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException;
}
java.io.Closeable是从JDK1.5版本引入的,作为DataSource肯定会涉及资源的清理释放,java.io.Closeable正是用于关闭数据源、释放对象保存的资源。从JDK1.7版本开始引入java.lang.AutoCloseable,并且java.io.Closeable接口继承自java.lang.AutoCloseable,它的出现是为了更好地管理资源,尤其是释放资源,配合try-with-resources语法进行更为精简方便的资源释放及异常管理。
HikariDataSource继承了HikariConfig,并且实现了javax.sql扩展包中的DataSource接口。作为DriverManager设施的替代项,DataSource对象是获取连接的首选方法,实现DataSource接口的对象通常在基于JavaTM Naming and Directory Interface(JNDI)的命名服务中注册。
HikariConfig是HikariCP的配置管理核心类。它又实现了HikariConfigMXBean接口,用来对外暴露与HikariCP配置相关的JMX监控和管理功能。
HikariDataSource的类图如下所示:
在创建连接的过程中,有几个细节性的问题可以从源码重点关注下。
第一个就是HikariDataSource获取连接getConnection的单例处理。在实现单例模式时,如果未考虑多线程的情况,很可能会造成实例化多次并且被不同对象持有。在JDK1.5或者更高的版本中,扩展了volatile的语义,使用了volatile关键字后,重排序被禁止,双重检查锁可以减少开销。如果没有volatile关键字则可能由于指令重排导致HikariPool对象在多线程下无法正确地初始化,volatile禁止了指令重排,并强制本地线程读取主存。由于数据库连接池处于一个被频繁调用的位置,Hikari的源码部分就是用双重检查锁进行单例初始化。
private final HikariPool fastPathPool;
private volatile HikariPool pool; // 注意这里引入了volatile
@Override
public Connection getConnection() throws SQLException
{
if (isClosed()) { // 检查数据源是否已经关闭,如果关闭则抛出异常
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
if (fastPathPool != null) { // 如果当前引用HikariPool不为空,则直接返回连接
return fastPathPool.getConnection();
}
// See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
HikariPool result = pool;
if (result == null) { // 双重检查锁代码
synchronized (this) {
result = pool;
if (result == null) {
validate(); // 参数校验,主要是检查参数是否合法并给予默认值
LOGGER.info("{} - Starting...", getPoolName());
try {
pool = result = new HikariPool(this); // 初始化连接池
this.seal();
}
catch (PoolInitializationException pie) {
if (pie.getCause() instanceof SQLException) {
throw (SQLException) pie.getCause();
}
else {
throw pie;
}
}
LOGGER.info("{} - Start completed.", getPoolName());
}
}
}
return result.getConnection();
}
上述代码中初始化了fastPathPool的值,fastPathPool是一个final成员,只能在构造函数中初始化。如果HikariDataSource中采用了HikariConfig进行构造初始化,那么它会提供对池的synchronization-free访问;如果HikariDataSource采用了no-args构造初始化,则必须在getConnection方法中延迟初始化池,这就需要同步开销。因为,HikariCP存在fastPathPool和简单的Pool,fastPathPool由于定义为final类型,所以不能在除构造函数之外任何地方进行设置。
第二个细节就是获取连接时的有效性检查。当从资源池里面获取到资源后,需要检查该资源的有效性,如果失效,则再次获取连接,这样就可以避免执行业务的时候报错。这部分工作是由HikariPool完成的,它通过PoolEntry是否已经被标记清除了、当前PoolEntry的存活时间是否过期及当前连接是否活着这3项进行判断,如果超时则关闭这个PoolEntry的连接,重置超时时间,再次获取连接。这部分的核心实现在HikariPool的getConnection方法里。
public Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
final long startTime = currentTime();
try {
long timeout = hardTimeout;
do {
// 从ConcurrentBag中借用连接,借用过程中会发生创建连接、连接过期、空闲等事情
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception // 中断,跳出循环,并抛出异常
}
final long now = currentTime(); // 记录当前时间
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) { // 有效性检查
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); // 关闭当前失效连接
timeout = hardTimeout - elapsedMillis(startTime); // 重置超时时间
}
else {
// 借用的资源若未过期未丢弃进入此逻辑,则设置metrics监控指标
metricsTracker.recordBorrowStats(poolEntry, startTime);
// 通过代理创建连接
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
}
} while (timeout > 0L);
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
在上述代码中,完成有效性检查的是isConnectionAlive,在这里用户可以自行配置心跳语句的检测(connectionTestQuery),如果追求极致性能,那么对于使用JDBC4的用户强烈建议不要配置心跳语句,而是采用HikariCP默认的com.mysql.jdbc.JDBC4Connection的isValid实现,因为它的实现是ping命令,一般来说采用原生ping命令的性能是select 1的两倍。getConnection方法,其心跳语句检测的isConnectionAlive是在其父类中实现的,核心代码如下:
boolean isConnectionAlive(final Connection connection)
{
try {
try {
setNetworkTimeout(connection, validationTimeout);
final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
if (isUseJdbc4Validation) { // 如果是JDBC4,则使用ping命令进行验证
return connection.isValid(validationSeconds);
}
try (Statement statement = connection.createStatement()) {
if (isNetworkTimeoutSupported != TRUE) {
setQueryTimeout(statement, validationSeconds); // 否则使用测试语句验证
}
statement.execute(config.getConnectionTestQuery());
}
}
finally {
setNetworkTimeout(connection, networkTimeout);
if (isIsolateInternalQueries && !isAutoCommit) {
connection.rollback();
}
}
return true;
}
catch (Exception e) {
lastConnectionFailure.set(e);
logger.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.",
poolName, connection, e.getMessage());
return false;
}
}
上述代码中有一个validationTimeout属性,默认值是3000毫秒,所以默认情况下final validation的值应该为1-5秒。又由于validationTimeout的值必须小于connectionTimeout(默认值是30000毫秒,如果小于250毫秒,则被重置回30秒),所以在默认情况下,调整validationTimeout却不调整connectionTimeout,validationSeconds的默认值应该是30毫秒。
如果是JDBC4,则使用isUseJdbc4Validation,用connection.isValid来验证连接的有效性,否则用connectionTestQuery查询语句来查询验证。
Java.sql.Connection的isValid()和isClosed的区别
- isValid:如果连接尚未关闭并且仍然有效,则返回true。驱动程序将提交一个关于该连接的查询,或者使用其他某种能够确切验证在调用此方法时连接是仍然有效的机制。由驱动程序提交的用来验证该连接的查询将在当前事务的上下文中执行。
- 参数:timeout。等待用来验证连接是否完成的数据库操作的时间,以秒为单位。如果在操作完成之前超时期满,则此方法返回false。0值表示不对数据库操作应用超时值。
- 返回:如果连接有效,则返回true,否则返回false。
- isClosed:查询此Connection对象是否已经被关闭。如果在连接上调用了close方法或者发生某些严重的错误,则连接被关闭。只有在调用了Connection.close方法之后被调用时,此方法才保证返回true。通常不能调用此方法来确定到数据库的连接是否有效的还是无效的。通过捕获在试图进行某一操作时可能抛出的异常,典型的客户端可以确定某一连接是无效的。
- 返回:如果此Connection对象是关闭的,则返回true;如果它仍然处于打开状态,则返回false。
连接的获取过程
- 实现了JDBC扩展包javax.sql的DataSource接口的HikariDataSource,执行getConnection方法时会进行Double-checked_locking单例,配置检查等流程,再向HikariPool资源池请求获取连接。
- HikariPool则向其lock-free的资源集合ConcurrentBag借用PoolEntry,若没有poolEntry则超时抛出异常,若有poolEntry则创建一个JDBC代理连接ProxyConnection。
- ProxyConnection是由ProxyFactory产生的,它是一个生产标准JDBC接口代理的工厂类,HikariDataSource最终获取的Connection连接就是代理工厂返回的JDBC物理连接,而poolEntry就是对这个物理连接的一对一封装。
归还连接
连接的归还和连接的借用是两个大致相反的过程,归还部分的代码如下所示:
@Override
public final void close() throws SQLException
{
// Closing statements can cause connection eviction, so this must run before the conditional below
closeStatements();
if (delegate != ClosedConnection.CLOSED_CONNECTION) {
leakTask.cancel();
try {
if (isCommitStateDirty && !isAutoCommit) {
delegate.rollback(); // 若存在脏提交或者没有自动提交,则连接回滚
lastAccess = currentTime();
LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", poolEntry.getPoolName(), delegate);
}
if (dirtyBits != 0) {
poolEntry.resetConnectionState(this, dirtyBits);
lastAccess = currentTime();
}
delegate.clearWarnings();
}
catch (SQLException e) {
// when connections are aborted, exceptions are often thrown that should not reach the application
if (!poolEntry.isMarkedEvicted()) {
throw checkException(e);
}
}
finally {
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess);
}
}
}
在ProxyConnection代理层获取到的连接,进行归还时调用了代理层的close方法。HikariCP归还连接是一系列没有返回值的void操作,ProxyConnection的close方法并没有直接调用JDBC的close方法,而是依次调用了PoolEntry的recycle方法,HikariPool的recycle方法及ConcurrentBag的require方法,这一系列方法传递的参数都是PoolEntry。
数据库连接池HikariCP的close方法返回的连接其实是封装在这个ProxyConnection代理连接中的,当调用它的时候,它只返回与池的连接,但是依然保持与数据库基础连接是打开的。数据库连接池通常都是以这种方式工作的,数据库连接池的性能优势就是来自于连接保持打开,通过代理链接,对于用户来说是透明的。如果需要关闭这个连接,可以将它先转换为ProxyConnection,然后调用它的unwrap方法,最后关闭这个内部连接。