Java 基础体系 · 第 84/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java 数据库连接池:HikariCP、容量、超时、泄漏和监控
数据库连接池解决的不是“让数据库更快”这一单一问题,而是复用数据库连接、限制并发连接数,并把连接创建、借用、归还和失效检测纳入统一生命周期管理。
在 Java 中,应用通常通过 JDBC 的 DataSource 获取连接:
try (Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement("select id, name from account where id = ?")) {
statement.setLong(1, 42L);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
System.out.printf("%d %s%n",
resultSet.getLong("id"),
resultSet.getString("name"));
}
}
}
Connection、PreparedStatement 和 ResultSet 都实现了 AutoCloseable。这里的 try-with-resources 不只是语法简化:它保证异常路径也会执行关闭操作。对于连接池来说,关闭池化连接通常意味着“归还连接”,而不是立刻断开底层 TCP 连接。
JDBC 规范定义了应用访问数据库的基础抽象,但没有规定连接池必须如何实现。连接池是 JDBC DataSource 之上的常见实现。Jakarta Persistence(JPA)则通过 EntityManager、事务和持久化上下文提供 ORM 抽象;JPA 实现通常再使用一个 JDBC DataSource,因此 JPA 事务边界最终仍然会影响连接的借用时长。
连接池到底复用了什么
一次数据库访问至少涉及以下对象和资源:
- 应用线程。
- 池中的逻辑连接对象。
- 底层数据库连接,通常包含 TCP 连接、认证状态、会话状态。
- 数据库端的会话、工作内存和执行资源。
- SQL 执行期间的锁、游标、结果集和事务状态。
“连接池复用”主要复用第 3 项及其相关数据库会话。若每次请求都新建连接,流程通常是:
应用线程
-> 创建 TCP 连接
-> TLS 握手(如果启用)
-> 数据库认证
-> 创建数据库会话
-> 执行 SQL
-> 关闭连接
连接池把它变成:
应用启动或首次需要时创建底层连接
-> 应用借用逻辑连接
-> 执行 SQL
-> 关闭逻辑连接并归还池
-> 后续请求重复使用底层连接
因此,连接池减少的是连接建立成本,并且为数据库并发设置了一个上限。它不会绕过 SQL 执行、锁竞争、磁盘 IO 或数据库 CPU 饱和。
连接的几个状态
可以把一个池化连接抽象成以下状态:
stateDiagram-v2
[*] --> Creating
Creating --> Idle: 创建并通过校验
Creating --> Failed: 创建失败
Idle --> Borrowed: getConnection()
Borrowed --> Idle: close()
Borrowed --> Evicting: 超过 maxLifetime 或发现失效
Idle --> Evicting: idleTimeout / keepalive 检查失败
Evicting --> Closed: 物理关闭
Failed --> Closed: 放弃创建
Closed --> Creating: 池仍需要补足容量
几个状态转换容易被误解:
Idle表示连接在池中可立即借用,不表示数据库事务一定已经提交。Borrowed表示连接已经交给应用代码,连接池通常无法控制应用是否正在执行 SQL。- 调用池化连接的
close()通常触发归还。 - 连接因
maxLifetime被淘汰时,通常要等它归还后再安全关闭;正在使用的连接不会被简单地强行切断。 - 如果应用没有关闭连接,连接会一直处于
Borrowed,这就是连接泄漏的典型表现。
HikariCP 的核心模型
HikariCP 是一个 JDBC DataSource 实现。应用不应依赖 HikariCP 的内部连接类,而应依赖标准接口:
DataSource dataSource;
try (Connection connection = dataSource.getConnection()) {
// 使用 JDBC
}
HikariCP 管理的关键数量包括:
- 总连接数:池当前拥有的底层连接数。
- 空闲连接数:当前未被借用的连接数。
- 活动连接数:当前已被应用借用的连接数。
- 等待线程数:正在等待连接的应用线程数。
它们满足近似关系:
在没有连接创建或淘汰瞬间时,这个关系最直观。Total 通常不超过 maximumPoolSize,而 Active 达到上限且没有连接归还时,新的 getConnection() 调用就会等待,直到拿到连接或超时。
一个最小可运行示例
下面的示例使用 H2 作为演示数据库。生产环境只需替换 JDBC 驱动、JDBC URL、用户名和密码;HikariCP 的使用方式不因数据库类型改变。
Maven 依赖:
<dependencies>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>7.0.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
<scope>runtime</scope>
</dependency>
</dependencies>
示例版本应与项目实际使用的 JDK、驱动和发布渠道核对。Java 25 LTS 不会改变 JDBC 的基本连接生命周期;连接池仍然是普通 Java 库,关键兼容性来自 HikariCP、数据库驱动和数据库服务器本身。
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public final class HikariExample {
public static void main(String[] args) throws SQLException {
HikariConfig config = new HikariConfig();
config.setPoolName("orders-pool");
config.setJdbcUrl("jdbc:h2:mem:orders;DB_CLOSE_DELAY=-1");
config.setUsername("sa");
config.setPassword("");
config.setMaximumPoolSize(8);
config.setMinimumIdle(8);
config.setConnectionTimeout(2_000);
config.setValidationTimeout(1_000);
config.setMaxLifetime(25 * 60_000L);
config.setKeepaliveTime(2 * 60_000L);
try (HikariDataSource dataSource = new HikariDataSource(config)) {
createSchema(dataSource);
insertOrder(dataSource, 1001L, "created");
printOrder(dataSource, 1001L);
}
}
private static void createSchema(DataSource dataSource) throws SQLException {
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("""
create table orders (
id bigint primary key,
status varchar(30) not null
)
""")) {
statement.executeUpdate();
}
}
private static void insertOrder(
DataSource dataSource, long id, String status) throws SQLException {
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"insert into orders(id, status) values (?, ?)")) {
statement.setLong(1, id);
statement.setString(2, status);
statement.executeUpdate();
}
}
private static void printOrder(
DataSource dataSource, long id) throws SQLException {
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"select id, status from orders where id = ?")) {
statement.setLong(1, id);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
System.out.printf("id=%d, status=%s%n",
resultSet.getLong("id"),
resultSet.getString("status"));
}
}
}
}
}
预期输出类似:
id=1001, status=created
try (HikariDataSource dataSource = ...) 很重要。应用关闭时,池需要关闭所有底层连接。Web 应用通常把 HikariDataSource 作为应用级单例,由容器在应用停止时关闭,而不是每个请求创建一个池。
连接池容量:不是越大越快
maximumPoolSize 是池允许创建的最大底层连接数。它同时限制:
- 应用最多有多少个线程可以同时持有数据库连接;
- 数据库最多可能看到多少个来自该应用实例的连接;
- 数据库需要为这些会话保留多少资源。
如果设置为 1000,并不能让一个只能处理 32 个并发数据库操作的数据库变成能处理 1000 个。多余连接通常只会增加排队、锁竞争、上下文切换和数据库内存压力。
连接占用时间和 Little 定律
设:
- :单位时间内完成的数据库事务数;
- :一个事务从借用连接到归还连接的平均时间;
- :平均同时占用连接的数量。
根据 Little 定律:
例如:
- 吞吐量为每秒 120 个事务;
- 每个事务平均占用连接 35 ms,即 秒。
则:
这意味着在稳定状态下,平均约需要 4.2 个连接支撑该吞吐量。不能因此直接把池设为 5,因为平均值没有覆盖尾延迟、突发流量、事务抖动和连接创建过程。
如果同一事务的连接占用时间在高峰期接近 120 ms:
此时池规模可能需要接近 16 或更高才能减少应用侧等待,但这只是容量实验的起点,不是普适配置。若数据库在 8 个并发查询时已经饱和,把池增大到 32 只会把更多线程推入数据库内部排队。
另一个重要约束:数据库总连接预算
设数据库允许应用使用的连接预算为 ,部署了 个应用实例,每个实例最大池大小为 ,还要预留管理连接和其他服务连接 ,则必须满足:
例如:
- 数据库允许 200 个连接;
- 预留 40 个给管理工具、迁移任务和其他服务;
- 应用部署 8 个实例。
那么每个实例的最大池大小至少应满足:
因此:
如果每个实例配置 40 个连接,理论上可能产生 320 个连接,应用扩容后会把数据库连接名额耗尽。连接池配置必须按“实例数乘以池大小”计算,而不是只看单机压测。
minimumIdle 的含义
minimumIdle 是池希望维持的最小空闲连接数。它不等于最小并发数,也不等于数据库必须始终存在的连接数。
当 minimumIdle 小于 maximumPoolSize 时,池可以在低负载时收缩空闲连接;当流量上升时,再逐步创建连接。这样可以减少低峰连接占用,但突发请求可能承担连接创建延迟。
HikariCP 默认倾向于让 minimumIdle 与 maximumPoolSize 保持固定大小。官方文档通常建议不要为追求动态收缩而显式设置 minimumIdle,因为固定大小的池具有更稳定的延迟;是否采用固定大小仍取决于实例数量、数据库连接预算和弹性环境。
一个常见取舍是:
config.setMaximumPoolSize(16);
// 固定大小池:启动后逐步维持 16 个连接
config.setMinimumIdle(16);
或者:
config.setMaximumPoolSize(16);
// 允许低峰收缩,突发时可能创建连接
config.setMinimumIdle(2);
不能把 minimumIdle=2 理解为“池最多只使用 2 个连接”;上限仍然是 maximumPoolSize=16。
连接借用过程和容量耗尽
当线程调用 dataSource.getConnection() 时,典型流程如下:
sequenceDiagram
participant T as 应用线程
participant P as HikariCP
participant DB as 数据库
T->>P: getConnection()
alt 有空闲连接
P-->>T: 返回池化连接
else 未达到 maximumPoolSize
P->>DB: 创建底层连接
DB-->>P: 连接成功
P-->>T: 返回池化连接
else 已达到上限
T->>P: 等待空闲连接
alt 其他线程归还连接
P-->>T: 返回归还的连接
else 超过 connectionTimeout
P-->>T: SQLException
end
end
T->>DB: 执行 SQL
DB-->>T: 结果或错误
T->>P: close()
P-->>P: 重置连接状态并回收到池中
当池耗尽时,线程等待的原因可能是:
- SQL 本身执行很慢;
- 事务持有连接但等待锁;
- 代码读取结果集很慢;
- 连接被借出后没有关闭;
- 数据库不可用,连接创建长期失败;
- 数据库连接数已达到服务器上限。
因此,“连接池等待超时”不是数据库查询超时的同义词。它只说明线程在池中没有及时拿到可用连接,真正根因可能发生在更早的 SQL、事务或故障路径上。
超时:不同计时器控制不同阶段
连接池中最容易误配的是超时。每个超时控制的对象不同,不能用一个参数替代全部超时。
connectionTimeout
connectionTimeout 控制调用 getConnection() 时,线程最多等待多久。它覆盖的主要等待包括:
- 等待其他线程归还连接;
- 等待池创建一个新连接;
- 等待池内部完成可用连接分配。
HikariCP 的默认值通常是 30 秒,允许的最小值是 250 ms。示例:
config.setConnectionTimeout(2_000);
这表示拿不到连接时,约 2 秒后抛出 SQLException。它不限制 SQL 执行时间,也不保证数据库服务器在 2 秒后停止一个已经开始执行的查询。
如果设置过短,正常的数据库短暂抖动也会快速转化为请求失败;如果设置过长,请求线程可能长时间堆积,最终形成线程池耗尽。
validationTimeout
validationTimeout 控制连接可用性校验的等待时间,通常必须小于 connectionTimeout,最小值也通常是 250 ms:
config.setValidationTimeout(1_000);
连接校验不等于执行业务 SQL。HikariCP 会优先使用 JDBC 驱动的 Connection.isValid(timeout);某些场景也可以配置测试查询。测试查询应尽量简单,并符合目标数据库语法。
SQL 执行超时
SQL 超时属于 JDBC Statement 或数据库驱动层面:
try (PreparedStatement statement = connection.prepareStatement(
"select id, status from orders where id = ?")) {
statement.setQueryTimeout(3); // 单位:秒
statement.setLong(1, 1001L);
try (ResultSet resultSet = statement.executeQuery()) {
// 处理结果
}
}
setQueryTimeout 的实际行为依赖 JDBC 驱动和数据库。它通常会请求驱动在超时后中断查询,但不应假设所有数据库都能在精确时间点强制终止服务器端执行。
数据库还可能有自己的语句超时、锁等待超时、事务空闲超时和网络 socket 超时。这些参数需要与 JDBC 驱动文档一起验证。
maxLifetime
maxLifetime 控制一个物理连接在池中的最长生命周期。HikariCP 默认值通常为 30 分钟;设置为 0 表示不限制,但通常不建议在数据库或网络设备有连接寿命限制时这样做。
config.setMaxLifetime(25 * 60_000L);
如果云数据库、代理、负载均衡器或防火墙会在 30 分钟时关闭连接,把池的 maxLifetime 设为略短于基础设施的限制,可以让池主动轮换连接,而不是等请求碰到被中间设备关闭的连接。
HikariCP 会对连接生命周期做轻微的随机衰减,避免所有连接在同一时刻批量淘汰。这个机制不能替代合理的生命周期配置。
idleTimeout
idleTimeout 控制空闲连接在可收缩池中的回收时间。它主要在 minimumIdle < maximumPoolSize 时有意义;固定大小池中,池不能把连接数降到最小空闲数以下。
config.setMinimumIdle(2);
config.setIdleTimeout(5 * 60_000L);
HikariCP 对 idleTimeout 有最小值限制,并允许回收时间存在一定波动,因此不能把它当作精确的定时器。
keepaliveTime
keepaliveTime 用于周期性检查空闲连接,避免连接被数据库或网络设备因长期空闲而断开:
config.setKeepaliveTime(2 * 60_000L);
它通常只作用于空闲连接,并且必须小于 maxLifetime,还受到最小值限制。Keepalive 不是 SQL 性能优化,也不是连接泄漏检测;它会产生额外网络和数据库请求,因此不应设置得过于频繁。
超时关系可以概括为:
getConnection() 等待连接 -> connectionTimeout
连接可用性检测 -> validationTimeout
已借到连接后执行 SQL -> Statement.setQueryTimeout / 驱动 / 数据库参数
连接在池中的最大年龄 -> maxLifetime
空闲连接收缩 -> idleTimeout
空闲连接保活 -> keepaliveTime
借出时间过长的诊断 -> leakDetectionThreshold
事务边界决定连接占用时长
连接池容量计算中,最容易漏掉的是:连接通常会从事务开始一直持有到事务结束,而不只是覆盖某条 SQL。
JDBC 事务示例:
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try {
debit(connection, 1001L, 10);
credit(connection, 2002L, 10);
connection.commit();
} catch (Exception e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
}
这里的 finally 和外层 try-with-resources 各有职责:
rollback()防止异常路径留下未完成事务;setAutoCommit(true)把连接状态恢复到池可以安全交给下一个调用者的状态;close()把连接归还池。
池化连接归还时,HikariCP 会重置它能够识别的部分状态,例如自动提交模式、只读属性、事务隔离级别、网络超时等。但应用不应依赖池替自己修复业务层状态。数据库会话变量、临时表、服务端 prepared statement、特定驱动属性等状态可能需要显式清理或由驱动处理。
以下代码会显著放大连接占用:
try (Connection connection = dataSource.getConnection()) {
updateOrder(connection);
Thread.sleep(5_000); // 连接仍被持有
callRemoteService(); // 网络调用期间仍占用数据库连接
connection.commit();
}
如果有 16 个并发请求,每个请求都在持有连接后等待远程服务 5 秒,那么池很快就会出现等待,即使真正的 SQL 只用了几毫秒。正确的边界通常是:先完成不需要数据库连接的远程调用和计算,再在尽可能短的事务中执行数据库操作。
JPA 中的对应问题
JPA 使用 EntityManager 和事务,但连接的具体借用时机由 JPA 实现、事务类型和连接获取模式决定。应用不应把“打开 EntityManager”简单等同于“必然一直占用一个 JDBC 连接”,也不应据此忽略事务边界。
典型事务代码:
@Transactional
public void changeStatus(long id) {
Order order = entityManager.find(Order.class, id);
order.setStatus("PAID");
}
在这种模型中,事务注解通常由框架管理提交和回滚,应用不应手动关闭容器管理的 EntityManager。但以下操作仍可能延长连接或事务生命周期:
- 在事务中执行复杂查询;
- 延迟加载实体关联;
- 在事务内遍历大量结果;
- 在事务内调用外部 HTTP 服务;
- 忘记结束事务;
- 在异步线程中继续访问事务上下文。
JPA 规范和具体实现之间存在边界:JPA 规定持久化语义和事务交互,但不规定 HikariCP 的具体池参数。连接池参数应在应用服务器或框架提供的 DataSource 层配置。
连接泄漏:借出后没有归还
连接泄漏是指应用获取了连接,但在所有执行路径上都没有将其关闭。池不会无限创建连接,因此泄漏的最终表现通常是:
ActiveConnections 持续接近 maximumPoolSize
IdleConnections 接近 0
ThreadsAwaitingConnection 持续上升
getConnection() 最终因 connectionTimeout 失败
最典型的错误代码:
Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement("select count(*) from orders");
ResultSet resultSet = statement.executeQuery();
// 忘记关闭 resultSet、statement 和 connection
正确写法:
try (Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement("select count(*) from orders");
ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
System.out.println(resultSet.getLong(1));
}
}
资源关闭顺序是反向嵌套顺序:先关闭 ResultSet,再关闭 PreparedStatement,最后关闭 Connection。try-with-resources 能在正常返回、SQL 异常和运行时异常时执行关闭。
常见泄漏路径
异常路径没有关闭
Connection connection = dataSource.getConnection();
try {
executeBusinessSql(connection);
} catch (SQLException e) {
log.error("database error", e);
return; // connection 没有关闭
}
只关闭了语句,没有关闭连接
try (Connection connection = dataSource.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("...")) {
statement.execute();
}
} // 正确
而下面是不完整的:
Connection connection = dataSource.getConnection();
try (PreparedStatement statement = connection.prepareStatement("...")) {
statement.execute();
}
// connection 仍然没有关闭
返回了仍然依赖连接的结果集
ResultSet loadOrders() throws SQLException {
Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement("select * from orders");
return statement.executeQuery();
}
调用者拿到 ResultSet 时,连接、语句和结果集的生命周期已经跨越方法边界,异常和提前退出都很难正确处理。更安全的做法是方法内部遍历结果集,转换成领域对象后再关闭全部 JDBC 资源。
把连接交给异步任务
try (Connection connection = dataSource.getConnection()) {
executor.submit(() -> useConnection(connection));
}
异步任务可能在外层方法关闭连接之后执行,也可能因为队列等待而长时间持有连接。异步任务应在任务内部借用和关闭连接,而不是跨线程传递连接。
泄漏检测的含义和局限
HikariCP 可以通过 leakDetectionThreshold 报告借出时间过长的连接:
config.setLeakDetectionThreshold(10_000);
该阈值通常至少为 2 秒,设置为 0 表示禁用。超过阈值时,HikariCP 会记录连接借出时的调用栈,帮助定位“是谁借用了连接”。
它检测的是:
一个连接被借出后,经过阈值时间仍未归还。
它不能直接证明:
这一定是永久泄漏。
例如,下面的长事务会触发告警,但资源最终可能会归还:
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
executeLargeBatch(connection); // 运行 20 秒
connection.commit();
}
因此告警日志的正确解读是:
- 查看借出位置的堆栈;
- 找出该调用是否最终关闭连接;
- 检查期间是否执行了慢 SQL、锁等待或外部调用;
- 对照
ActiveConnections、等待线程数和数据库活动会话; - 决定是修复泄漏,还是缩短合法但过长的事务。
不应在生产环境把阈值设置得极低并把每条告警都当成泄漏。阈值应高于正常请求和事务的合理连接占用时间,同时低于能够耗尽池的危险时长。它是诊断工具,不是释放泄漏连接的回收器。
HikariCP 的连接状态重置
连接池把一个连接归还给下一个调用者之前,必须尽量消除前一个调用者留下的状态。下面这些状态尤其危险:
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
connection.setReadOnly(true);
connection.setTransactionIsolation(
Connection.TRANSACTION_SERIALIZABLE);
// 业务操作
}
如果这些设置没有被正确恢复,下一个请求可能意外地:
- 处于手动提交模式;
- 以只读模式执行;
- 使用过高的隔离级别;
- 继承前一个请求的事务状态;
- 看到未预期的会话级配置。
应用应在自定义 JDBC 操作中明确事务处理和异常回滚,不应假设“连接池一定知道所有数据库特有状态”。对于数据库特有的 session 设置,应统一封装并验证归还后的行为。
连接失效、数据库重启和网络中断
连接池中的连接可能因为以下原因变得不可用:
- 数据库重启;
- 数据库主动关闭空闲会话;
- 防火墙或负载均衡器清理空闲 TCP 连接;
- NAT 映射失效;
- 网络短暂中断;
- 数据库达到连接上限;
- 驱动或 TLS 层发生错误。
一次典型故障路径如下:
数据库或网络关闭连接
-> 池中仍保留该物理连接
-> 应用借用连接
-> 校验发现失败,或首次 SQL 失败
-> 连接被淘汰
-> 池尝试创建替代连接
-> 数据库不可用时,新的借用请求等待并最终超时
maxLifetime 可以减少遇到基础设施强制断开的问题,但不能修复数据库不可用。keepaliveTime 可以减少空闲连接被网络设备静默清理的概率,但不能保证网络永远可靠。
应用层应把数据库异常分类处理:
- 连接获取超时:通常是池耗尽、连接创建失败或数据库不可用;
- SQL 执行异常:可能是语法、约束、锁、超时或连接失效;
- 事务提交异常:不能简单重试,必须判断事务是否已经在数据库端提交;
- 可重试错误:必须结合数据库错误码、幂等性和事务边界设计。
尤其要注意:提交请求在网络断开时可能出现“客户端不知道结果”的不确定状态。盲目重试可能造成重复扣款、重复写入等业务错误。
监控:先看池,再看数据库,再看请求
仅记录 SQL 日志不足以判断连接池问题。至少需要同时观察三层指标。
HikariCP 连接池指标
启用 JMX:
config.setRegisterMbeans(true);
随后可以通过 JMX 查看池的运行状态。常见属性包括:
ActiveConnectionsIdleConnectionsTotalConnectionsThreadsAwaitingConnection
实际 MBean 名称与池名相关,通常可在 JConsole、VisualVM 或其他 JMX 客户端中查看。例如池名设置为 orders-pool 后,名称会包含该池名和 HikariCP 的池对象。
也可以在应用中暴露指标。HikariCP 支持通过指标注册表或指标追踪工厂接入监控系统,但具体方式取决于使用的是 Micrometer、Dropwizard Metrics 还是框架集成,不应把某个框架的配置写法当成 HikariCP 标准 API。
如何解释指标组合
| 现象 | 更可能的原因 |
|---|---|
| Active 接近上限,等待线程持续上升 | SQL 慢、事务过长、锁等待或连接泄漏 |
| Active 很低,但连接获取仍然失败 | 连接创建失败、数据库拒绝连接、连接校验异常 |
| Idle 长期为 0,Active 波动到上限 | 池可能偏小,也可能数据库操作过慢 |
| Total 不断下降后无法补足 | 数据库不可用、认证失败、连接数达到上限 |
| 泄漏告警增加,但 Active 不到上限 | 合法长事务或慢 SQL 可能被误判为泄漏 |
| 数据库端活动会话少,应用等待线程多 | 可能是连接创建、池锁争用或应用线程使用方式异常 |
这些指标必须结合时间窗口观察。单次看到 Active=maximumPoolSize 不足以证明池配置错误;高峰瞬间达到上限可能是正常现象,持续等待才说明容量或执行时间存在问题。
数据库端验证
数据库端必须查看真实活动会话、锁等待、执行计划和连接上限。不同数据库命令不同,例如 PostgreSQL 可以查询:
select
application_name,
state,
wait_event_type,
wait_event,
count(*)
from pg_stat_activity
where datname = current_database()
group by application_name, state, wait_event_type, wait_event
order by count(*) desc;
该 SQL 是 PostgreSQL 专用示例,不能直接用于 MySQL、Oracle 或 SQL Server。它可以帮助区分:
active:正在执行;idle:连接存在但没有执行语句;- 锁等待;
- 客户端空闲但事务未结束的状态。
如果 HikariCP 显示大量 Active,而数据库显示大量锁等待,增大池通常会加重锁竞争;应该先检查事务顺序、索引和锁持有时间。
一个可重复的诊断过程
假设日志中出现:
Connection is not available, request timed out after 2000ms
不要直接把 maximumPoolSize 从 16 改成 64。可以按以下顺序验证。
第一步:确认是否真的池耗尽
在同一时间窗口查看:
ActiveConnections
IdleConnections
TotalConnections
ThreadsAwaitingConnection
如果 Active=16、Idle=0、等待线程增长,说明池确实没有空闲连接,但仍不能区分慢 SQL、长事务和泄漏。
第二步:查看连接借用时长
临时启用:
config.setLeakDetectionThreshold(10_000);
如果出现借出堆栈,检查这些位置是否:
- 使用
try-with-resources; - 在异常路径回滚;
- 关闭
ResultSet和Statement; - 把连接传给异步任务;
- 在持有连接时调用外部服务。
第三步:对照数据库端活动
检查数据库是否存在:
- 大量锁等待;
- 慢查询;
- 全表扫描;
- 数据库 CPU 或 IO 饱和;
- 达到数据库连接上限;
- 大量“空闲但事务未结束”的会话。
第四步:测量连接占用时间
应记录至少两个时间点:
t0 = getConnection() 开始
t1 = getConnection() 成功
t2 = 最后一条 SQL 完成
t3 = close() 完成
其中:
- 是池等待或连接创建时间;
- 是借用后的数据库使用时间;
- 反映归还前的清理时间。
如果 高,先看池容量和创建连接;如果 高,先看 SQL、锁和事务;如果 t3 没有出现,优先怀疑泄漏或线程卡死。
第五步:再做容量调整
只有在确认数据库仍有处理能力、事务和 SQL 已经合理、连接确实是主要瓶颈后,才增加池容量。增加后必须重新检查数据库总连接预算:
并重新观察数据库 CPU、锁等待、响应时间和错误率。
固定池与弹性池的取舍
固定池的特点是连接数更稳定:
config.setMaximumPoolSize(16);
config.setMinimumIdle(16);
适合流量相对稳定、数据库连接预算明确的服务。代价是低峰期仍保留连接。
弹性池的特点是空闲时可以收缩:
config.setMaximumPoolSize(16);
config.setMinimumIdle(2);
config.setIdleTimeout(300_000);
适合实例数量变化大、低峰时间长或数据库连接成本较高的环境。代价是突发流量可能等待新连接建立,且连接频繁创建和销毁会增加数据库认证与网络开销。
无论哪种模式,都应避免每个请求创建一个 HikariDataSource:
// 错误:每次调用都创建一个连接池
void query() throws SQLException {
try (HikariDataSource dataSource = createDataSource();
Connection connection = dataSource.getConnection()) {
// ...
}
}
这段代码不仅失去池的复用价值,还会频繁创建和关闭数据库连接。正确结构通常是应用启动时创建一个 DataSource,请求只借用其中的连接。
监控与告警的边界
合理的告警不应只设置“连接数达到上限”。更有用的信号包括:
ThreadsAwaitingConnection持续大于 0;- 连接获取等待时间的 p95、p99 上升;
- Active/Total 长期接近 1;
- 连接创建失败次数增加;
- 泄漏检测告警出现;
- 数据库端锁等待和慢查询增加;
- 事务持续时间超过业务预期;
- 连接池耗尽错误与接口错误率同时上升。
告警恢复也需要验证。比如数据库恢复后,应确认:
- 新连接能够成功创建;
TotalConnections能补回目标范围;IdleConnections不再长期为零;- 等待线程数下降;
- 接口错误率恢复;
- 没有残留的长事务或泄漏连接。
直接重启应用可能让指标暂时恢复,但会掩盖根因,并在所有实例同时重启时对数据库造成连接创建高峰。重启应作为故障止损手段,而不是连接池诊断的替代品。
常见误解
“连接池越大,吞吐量越高”
错误。连接池只提供并发连接,数据库真正的处理能力受 CPU、IO、锁、索引、执行计划和事务冲突限制。池超过数据库有效并发后,通常增加排队而不是吞吐量。
“connectionTimeout 能终止慢 SQL”
错误。它只控制借用连接的等待时间。已经拿到连接的线程执行慢 SQL 时,必须使用 JDBC、驱动或数据库层面的语句超时。
“maxLifetime 能避免所有断连”
错误。它只能主动轮换达到生命周期上限的连接。数据库宕机、网络分区、认证失败和服务器连接上限仍会导致创建或执行失败。
“出现泄漏检测日志,就一定是代码忘记关闭”
不一定。长事务、锁等待、大结果集处理和外部调用都会让连接合法地持有很久。泄漏检测日志提供的是借出堆栈和超时证据,需要结合最终是否归还来判断。
“关闭池化连接会关闭数据库物理连接”
通常不是。对池化连接调用 close() 的语义是归还池。只有关闭 HikariDataSource,或连接被淘汰、失效时,才会关闭底层物理连接。
“JPA 管理事务后就不需要理解连接池”
错误。JPA 隐藏了部分 JDBC 细节,但事务持续时间、查询性能、延迟加载和外部调用仍会影响底层连接占用。连接池耗尽时,JPA 请求同样会失败。
一套可解释的初始配置
下面是一套具有明确含义的起点,而不是适用于所有系统的固定答案:
HikariConfig config = new HikariConfig();
config.setPoolName("orders-pool");
config.setJdbcUrl(jdbcUrl);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(16);
config.setMinimumIdle(16);
config.setConnectionTimeout(2_000);
config.setValidationTimeout(1_000);
config.setMaxLifetime(25 * 60_000L);
config.setKeepaliveTime(2 * 60_000L);
// 诊断阶段启用;正常运行时按长事务基线调整
config.setLeakDetectionThreshold(10_000);
config.setRegisterMbeans(true);
这套配置表达了以下假设:
- 单实例最多同时占用 16 个数据库连接;
- 池倾向于固定大小;
- 等待连接超过 2 秒就快速失败;
- 连接校验不应等待超过 1 秒;
- 基础设施可能在约 30 分钟左右清理连接,因此池提前轮换;
- 空闲连接每 2 分钟进行保活检查;
- 借用超过 10 秒会触发诊断日志。
它仍然需要通过压测和生产观测修正。最终配置应满足三个条件:
连接池设计的核心不是记住某个“推荐值”,而是把连接占用时间、数据库处理能力、实例数量、超时语义和故障恢复路径放在同一个模型中。HikariCP 负责高效管理连接生命周期,但 SQL、事务、数据库容量和应用资源关闭责任仍然属于应用工程本身。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java 属性测试与模糊测试:生成器、不变量、缩减和回归
- 下一篇:Java 数据库迁移:Flyway、Liquibase、版本、回滚和零停机
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论