本文介绍 Spring Boot 2 配置 HikariCP 数据库连接池的方法。
目录
- HikariCP 简介
- Spring Boot HikariCP
- 配置项说明
- 开发环境
- 代码示例
HikariCP 简介
HikariCP 是一个非常轻量级(130 KB 左右)且效率很高的 JDBC 连接池框架。
与已有的其它 JDBC 连接池技术(c3p0、dbcp2、tomcat、druid)相比,HikariCP 的速度非常快,因为它采用了以下技术:
- 字节码精简:优化代码,直至编译后的字节码最少;
- 优化代理和拦截器:减少代码量,如:HikariCP 的 Statement proxy 只有 100 行,是 BoneCP 的十分之一;
- 自定义集合类型
FastStatementList
代替ArrayList
:避免每次get()
调用都进行范围检查,避免调用remove()
时从头到尾扫描; - 自定义集合类型
ConcurrentBag
:提高并发读写效率; - 其他的一些微观优化:尽管几乎无法衡量,但这些优化相结合可提高整体性能。
Spring Boot HikariCP
Spring Boot 在 1.x 版本中默认使用 Tomcat JDBC 连接池,但到了 2.x 版本后就将 HikariCP 作为默认的数据库连接池(spring-boot-starter-jdbc
中包含了 HikariCP 连接池)。
Spring Boot 按照如下规则选用数据库连接池实现:
- 考虑到性能和并发,Spring Boot 优先使用 HikariCP;
- 其次选择 Tomcat 数据连接池;
- 如果 HikariCP 和 Tomcat 数据连接池都不可用,而 Commons DBCP2 可用,那么选择 Commons DBCP2。
可以通过设置 spring.datasource.type
属性绕过以上规则。
配置项说明
配置项:allow-pool-suspension
类型:java.lang.Boolean
构造器默认值:false
默认配置 validate
后的值:false
说明:控制池是否可以通过 JMX 暂停和恢复。
配置项:auto-commit
类型:java.lang.Boolean
构造器默认值:true
默认配置 validate
后的值:true
说明:自动提交从池中返回的连接。
配置项:catalog
类型:java.lang.String
构造器默认值:数据库驱动默认
默认配置 validate
后的值:null
说明:为支持 catalog 概念的数据库设置默认 catalog。
配置项:connection-init-sql
类型:java.lang.String
构造器默认值:null
默认配置 validate
后的值:null
说明: 该属性设置一个SQL语句,在将每个新连接创建后,将其添加到池中之前执行该语句。
配置项:connection-test-query
类型:java.lang.String
构造器默认值:
默认配置 validate
后的值:
说明:如果驱动程序支持 JDBC4,强烈建议不要设置此属性。
配置项:connection-timeout
类型:java.lang.Long
构造器默认值:SECONDS.toMillis(30) = 30000
默认配置 validate
后的值:30000
说明:等待来自池的连接的最大毫秒数,如果小于 250
毫秒,则被重置回 30
秒。
配置项:data-source-class-name
类型:java.lang.String
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:data-source-j-n-d-i
类型:java.util.Properties
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:driver-class-name
类型:java.lang.String
构造器默认值:null
默认配置 validate
后的值:null
说明:HikariCP 将尝试通过仅基于 jdbcUrl
的 DriverManager
解析驱动程序,但对于一些较旧的驱动程序, 还必须指定 driverClassName
。
配置项:health-check-properties.
类型:java.util.Properties
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:health-check-registry
类型:java.lang.Object
构造器默认值:
默认配置 validate
后的值:
说明:该属性允许指定池使用的 Codahale / Dropwizard HealthCheckRegistry 的实例来报告当前健康信息。
配置项:idle-timeout
类型:java.lang.Long
构造器默认值:MINUTES.toMillis(10) = 600000
默认配置 validate
后的值:600000
说明:连接允许在池中闲置的最长时间。如果 idleTimeout + 1 秒 > maxLifetime
且 maxLifetime > 0
,则会被重置为 0
(代表永远不会退出);如果 idleTimeout != 0
且小于 10
秒,则会被重置为 10
秒。
配置项:initialization-fail-timeout
类型:java.lang.Long
构造器默认值:1
默认配置 validate
后的值:1
说明:如果池无法成功初始化连接,则此属性控制池是否将 fail fast。
配置项:isolate-internal-queries
类型:java.lang.Boolean
构造器默认值:false
默认配置 validate
后的值:false
说明:是否在其自己的事务中隔离内部池查询,例如连接活动测试。
配置项:jdbc-url
类型:java.lang.String
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:leak-detection-threshold
类型:java.lang.Long
构造器默认值:0
默认配置 validate
后的值:0
说明:记录消息之前连接可能离开池的时间量,表示可能的连接泄漏。如果大于 0
且不是单元测试,则进一步判断:(leakDetectionThreshold < SECONDS.toMillis(2) or (leakDetectionThreshold > maxLifetime && maxLifetime > 0)
,会被重置为 0
,即如果要生效则必须大于 0
,而且不能小于 2
秒,而且当 maxLifetime > 0
时不能大于 maxLifetime
.
配置项:login-timeout
类型:java.lang.Integer
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:max-lifetime
类型:java.lang.Long
构造器默认值:MINUTES.toMillis(30) = 1800000
默认配置 validate
后的值:1800000
说明:池中连接最长生命周期,如果不等于 0
且小于 30
秒则会被重置回 30
分钟。
配置项:maximum-pool-size
类型:java.lang.Integer
构造器默认值:-1
默认配置 validate
后的值:10
说明:池中最大连接数,包括闲置和使用中的连接。如果小于 1
,则会被重置。当 minIdle <= 0
被重置为 DEFAULT_POOL_SIZE
则为 10
,如果 minIdle > 0
则重置为 minIdle
的值。
metric-registry
| java.lang.Object
| | | 该属性允许指定一个 Codahale / Dropwizard MetricRegistry 的实例,供池使用以记录各种指标。
配置项:metrics-tracker-factory
类型:com.zaxxer.hikari.metrics.MetricsTrackerFactory
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:minimum-idle
类型:java.lang.Integer
构造器默认值:-1
默认配置 validate
后的值:10
说明:池中维护的最小空闲连接数,如果小于 0
或大于 maximum-pool-size
则被重置为 maximum-pool-size
。
配置项:password
类型:java.lang.String
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:pool-name
类型:java.lang.String
构造器默认值:null
默认配置 validate
后的值:HikariPool-1
说明:连接池的用户定义名称,主要出现在日志记录和 JMX 管理控制台中以识别池和池配置。
配置项:read-only
类型:java.lang.Boolean
构造器默认值:false
默认配置 validate
后的值:false
说明:从池中获取的连接是否默认处于只读模式。
配置项:register-mbeans
类型:java.lang.Boolean
构造器默认值:false
默认配置 validate
后的值:false
说明:是否注册JMX管理Bean(MBeans)。
配置项:scheduled-executor
类型:java.util.concurrent.ScheduledExecutorService
构造器默认值:null
默认配置 validate
后的值:null
说明:此属性允许您设置将用于各种内部计划任务的 java.util.concurrent.ScheduledExecutorService
实例。
配置项:schema
类型:java.lang.String
构造器默认值:数据库驱动默认
默认配置 validate
后的值:null
说明:该属性为支持模式概念的数据库设置默认模式。
配置项:transaction-isolation
类型:java.lang.String
构造器默认值:null
默认配置 validate
后的值:null
说明:控制从池返回的连接的默认事务隔离级别。
配置项:username
类型:java.lang.String
构造器默认值:
默认配置 validate
后的值:
说明:
配置项:validation-timeout
类型:java.lang.Long
构造器默认值:SECONDS.toMillis(5) = 5000
默认配置 validate
后的值:5000
说明:连接将被测试活动的最大时间量,如果小于 250
毫秒,则会被重置回 5
秒。
开发环境
- JDK 8
- MySQL 8.x
代码示例
- 创建存储用户信息的数据表。
CREATE TABLE `test`.`user` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`account` VARCHAR(30) NOT NULL,
`name` VARCHAR(60) NOT NULL,
`birth` DATE NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC),
UNIQUE INDEX `account_UNIQUE` (`account` ASC));
创建 Spring Boot 工程,参考:IntelliJ IDEA 创建 Spring Boot 工程。
-
在生成的
pom
文件中添加以下依赖:-
mysql-connector-java
:MySQL 数据库驱动 -
spring-boot-starter-jdbc
:Spring JDBC 支持
-
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.6.RELEASE
tutorial.spring.boot
spring-boot-hikari
0.0.1-SNAPSHOT
spring-boot-hikari
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
8.0.19
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-maven-plugin
- 修改
application.yml
,添加数据源配置。
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=false
type: com.zaxxer.hikari.HikariDataSource
hikari:
pool-name: hikari-jdbc-pool
minimum-idle: 10
maximum-pool-size: 20
connection-timeout: 60000
idle-timeout: 60000
validation-timeout: 3000
max-lifetime: 600000
- 新建
DataSourceConfig
配置类,在工程创建时实例化一个 SpringJdbcTemplate
实例(SpringJdbcTemplate
是 Spring 框架提供的对 JDBC 的简单封装,JdbcOperations
是JdbcTemplate
实现的接口)。
package tutorial.spring.boot.hikari.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
public JdbcOperations jdbcOperations(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
- 编写领域模型类,通常类属性和表中字段一一对应。
package tutorial.spring.boot.hikari.domain;
import java.time.LocalDateTime;
import java.util.Objects;
public class User {
private Long id;
private String account;
private String name;
private LocalDateTime birth;
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDateTime getBirth() {
return birth;
}
public void setBirth(LocalDateTime birth) {
this.birth = birth;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(account, user.account) &&
Objects.equals(name, user.name) &&
Objects.equals(birth, user.birth);
}
@Override
public int hashCode() {
return Objects.hash(id, account, name, birth);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", account='" + account + '\'' +
", name='" + name + '\'' +
", birth=" + birth +
'}';
}
}
- 编写数据访问层(DAO)接口代码。
package tutorial.spring.boot.hikari.dao;
import tutorial.spring.boot.hikari.domain.User;
public interface UserDao {
/**
* 新增
*/
int insert(User user);
/**
* 查询
*/
User get(String account);
/**
* 更新
*/
int update(User user);
/**
* 删除
*/
int delete();
}
- 编写数据访问层(DAO)接口实现代码。
package tutorial.spring.boot.hikari.dao.impl;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.stereotype.Repository;
import tutorial.spring.boot.hikari.dao.UserDao;
import tutorial.spring.boot.hikari.domain.User;
import java.sql.ResultSet;
import java.sql.SQLException;
@Repository
public class UserDaoImpl implements UserDao {
private final JdbcOperations jdbcOperations;
public UserDaoImpl(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
}
@Override
public int insert(User user) {
String sql = "INSERT INTO `user` (`account`, `name`, `birth`) VALUES (?,?,?)";
return jdbcOperations.update(sql, user.getAccount(), user.getName(), user.getBirth());
}
@Override
public User get(String account) {
String sql = "SELECT `id`, `account`, `name`, `birth` FROM `user` WHERE `account`=?";
return jdbcOperations.queryForObject(sql, this::mapResult, account);
}
private User mapResult(ResultSet rs, int row)
throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setAccount(rs.getString("account"));
user.setName(rs.getString("name"));
user.setBirth(rs.getTimestamp("birth").toLocalDateTime());
return user;
}
@Override
public int update(User user) {
String sql = "UPDATE `user` SET `name`=?, `birth`=? WHERE `account`=?";
return jdbcOperations.update(sql, user.getName(), user.getBirth(), user.getAccount());
}
@Override
public int delete() {
String sql = "DELETE FROM `user`";
return jdbcOperations.update(sql);
}
}
- 编写单元测试。
package tutorial.spring.boot.hikari.dao;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import tutorial.spring.boot.hikari.domain.User;
import java.time.LocalDateTime;
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserDaoTest {
private static User user;
@Autowired
private UserDao userDao;
@BeforeAll
public static void init() {
user = new User();
user.setAccount("admin1");
user.setName("Administrator");
LocalDateTime birth = LocalDateTime.of(1985, 1, 1, 0, 0, 0);
user.setBirth(birth);
}
@Test
@Order(1)
public void testNotNull() {
Assertions.assertNotNull(userDao);
}
@Test
@Order(2)
public void testInsert() {
Assertions.assertEquals(1, userDao.insert(user));
}
@Test
@Order(3)
public void testGet() {
User result = userDao.get(user.getAccount());
System.out.println(result);
Assertions.assertEquals(user.getAccount(), result.getAccount());
Assertions.assertEquals(user.getName(), result.getName());
Assertions.assertEquals(user.getBirth(), result.getBirth());
}
@Test
@Order(4)
public void testUpdate() {
user.setName("Master");
user.setBirth(LocalDateTime.of(1970, 1, 1, 0, 0, 0));
Assertions.assertEquals(1, userDao.update(user));
}
@Test
@Order(5)
public void testDelete() {
Assertions.assertTrue(userDao.delete() > 0);
}
}
测试结果略。