5.2
|
| 连接池 | 适用业务 | 目标 |
|---|---|---|
corePool |
核心交易、订单、支付 | 保证低延迟和高可用 |
readPool |
查询、列表、详情页 | 隔离查询流量 |
reportPool |
统计、导出、分析 | 隔离慢查询和长连接 |
jobPool |
定时任务、批处理 | 防止任务占满在线资源 |
当系统使用主从库时,推荐将连接池和数据源职责一起拆分:
写请求 -> writeDataSource -> Hikari writePool -> 主库
读请求 -> readDataSource -> Hikari readPool -> 从库
架构价值:
多租户系统容易走入一个误区:
“每个租户一个连接池。”
除非租户数量非常少、且资源隔离强需求极高,否则这种模式很容易失控:
更合理的策略通常是:
HikariCP 负责的是应用内连接复用,不是数据库治理平台。
如果你还需要下面这些能力:
更适合交给以下层完成:
架构原则是:
连接池负责应用内高效借还连接
代理层负责数据库流量治理
下面是一份适合绝大多数线上 Spring Boot 服务的 HikariCP 基线配置:
spring:
datasource:
url: jdbc:mysql://mysql-prod:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: app_user
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: order-main-pool
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 2000
validation-timeout: 1000
idle-timeout: 300000
max-lifetime: 1740000
keepalive-time: 120000
auto-commit: false
initialization-fail-timeout: 1
register-mbeans: true
这份配置的设计意图是:
maxLifetime + keepaliveTime 提升长连接稳定性适用于读写分离或业务隔离场景:
app:
datasource:
write:
jdbc-url: jdbc:mysql://mysql-write:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: order_rw
password: ${WRITE_DB_PASSWORD}
maximum-pool-size: 16
minimum-idle: 4
read:
jdbc-url: jdbc:mysql://mysql-read:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: order_ro
password: ${READ_DB_PASSWORD}
maximum-pool-size: 24
minimum-idle: 6
report:
jdbc-url: jdbc:mysql://mysql-report:3306/report_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: report_user
password: ${REPORT_DB_PASSWORD}
maximum-pool-size: 6
minimum-idle: 1
无论是 MyBatis 还是 JPA,下面这些原则都成立:
连接池只能提升“借还效率”,不能修复“坏事务设计”。
下面给出一个更贴近真实生产的 Spring Boot 示例,涵盖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
package com.example.order.config;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app.datasource")
public class MultiDataSourceProperties {
private PoolConfig write = new PoolConfig();
private PoolConfig read = new PoolConfig();
private PoolConfig report = new PoolConfig();
public PoolConfig getWrite() {
return write;
}
public void setWrite(PoolConfig write) {
this.write = write;
}
public PoolConfig getRead() {
return read;
}
public void setRead(PoolConfig read) {
this.read = read;
}
public PoolConfig getReport() {
return report;
}
public void setReport(PoolConfig report) {
this.report = report;
}
public static class PoolConfig {
@NotBlank
private String jdbcUrl;
@NotBlank
private String username;
@NotBlank
private String password;
@Min(1)
private int maximumPoolSize = 10;
@Min(0)
private int minimumIdle = 2;
public String getJdbcUrl() {
return jdbcUrl;
}
public void setJdbcUrl(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMaximumPoolSize() {
return maximumPoolSize;
}
public void setMaximumPoolSize(int maximumPoolSize) {
this.maximumPoolSize = maximumPoolSize;
}
public int getMinimumIdle() {
return minimumIdle;
}
public void setMinimumIdle(int minimumIdle) {
this.minimumIdle = minimumIdle;
}
}
}
package com.example.order.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
@EnableConfigurationProperties(MultiDataSourceProperties.class)
public class DataSourceConfig {
@Bean(name = "writeDataSource")
public DataSource writeDataSource(MultiDataSourceProperties properties) {
return createDataSource("write-pool", properties.getWrite());
}
@Bean(name = "readDataSource")
public DataSource readDataSource(MultiDataSourceProperties properties) {
return createDataSource("read-pool", properties.getRead());
}
@Bean(name = "reportDataSource")
public DataSource reportDataSource(MultiDataSourceProperties properties) {
return createDataSource("report-pool", properties.getReport());
}
@Bean
public JdbcTemplate writeJdbcTemplate(@Qualifier("writeDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcTemplate readJdbcTemplate(@Qualifier("readDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcTemplate reportJdbcTemplate(@Qualifier("reportDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
private DataSource createDataSource(String poolName, MultiDataSourceProperties.PoolConfig props) {
HikariConfig config = new HikariConfig();
config.setPoolName(poolName);
config.setJdbcUrl(props.getJdbcUrl());
config.setUsername(props.getUsername());
config.setPassword(props.getPassword());
config.setMaximumPoolSize(props.getMaximumPoolSize());
config.setMinimumIdle(props.getMinimumIdle());
config.setConnectionTimeout(2000);
config.setValidationTimeout(1000);
config.setIdleTimeout(300000);
config.setMaxLifetime(1740000);
config.setKeepaliveTime(120000);
config.setAutoCommit(false);
config.setInitializationFailTimeout(1);
config.setRegisterMbeans(true);
return new HikariDataSource(config);
}
}
下面这个例子展示了两个关键原则:
package com.example.order.service;
import com.example.order.client.InventoryClient;
import com.example.order.model.CreateOrderCommand;
import com.example.order.repository.OrderRepository;
import com.example.order.repository.StockRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderApplicationService {
private final InventoryClient inventoryClient;
private final OrderRepository orderRepository;
private final StockRepository stockRepository;
public OrderApplicationService(
InventoryClient inventoryClient,
OrderRepository orderRepository,
StockRepository stockRepository) {
this.inventoryClient = inventoryClient;
this.orderRepository = orderRepository;
this.stockRepository = stockRepository;
}
public Long createOrder(CreateOrderCommand command) {
inventoryClient.checkSellable(command.getProductId(), command.getQuantity());
return doCreateOrder(command);
}
@Transactional
protected Long doCreateOrder(CreateOrderCommand command) {
stockRepository.deduct(command.getProductId(), command.getQuantity());
return orderRepository.insert(command);
}
}
如果把 inventoryClient.checkSellable(...) 放进事务里,会带来什么问题?
这是非常典型的连接池雪崩前兆。
对账、物流明细、埋点入库等场景里,很多团队还在循环执行单条 insert。
更合理的方式是使用 JDBC batch:
package com.example.order.repository;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class OrderEventRepository {
private final JdbcTemplate writeJdbcTemplate;
public OrderEventRepository(JdbcTemplate writeJdbcTemplate) {
this.writeJdbcTemplate = writeJdbcTemplate;
}
public void batchInsert(List<OrderEventDO> events) {
String sql = """
insert into order_event(order_id, event_type, event_time, payload)
values (?, ?, ?, ?)
""";
writeJdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
OrderEventDO event = events.get(i);
ps.setLong(1, event.getOrderId());
ps.setString(2, event.getEventType());
ps.setObject(3, event.getEventTime());
ps.setString(4, event.getPayload());
}
@Override
public int getBatchSize() {
return events.size();
}
});
}
}
前提是 JDBC URL 打开:
rewriteBatchedStatements=true
这可以显著缩短连接占用时间,提高写入吞吐。
package com.example.order.query;
import java.time.LocalDate;
import java.util.List;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderReportQueryService {
private final JdbcTemplate reportJdbcTemplate;
public OrderReportQueryService(@Qualifier("reportJdbcTemplate") JdbcTemplate reportJdbcTemplate) {
this.reportJdbcTemplate = reportJdbcTemplate;
}
public List<DailyOrderStat> queryDailyStats(LocalDate beginDate, LocalDate endDate) {
String sql = """
select stat_date, count(*) as order_count, sum(pay_amount) as total_amount
from order_summary
where stat_date between ? and ?
group by stat_date
order by stat_date
""";
return reportJdbcTemplate.query(
sql,
(rs, rowNum) -> new DailyOrderStat(
rs.getDate("stat_date").toLocalDate(),
rs.getLong("order_count"),
rs.getBigDecimal("total_amount")),
beginDate,
endDate
);
}
}
这类长查询和核心订单写库使用不同数据源、不同池,是非常必要的。
连接池调优绝不是单点工作,必须和线程池、限流、重试、缓存、降级联动设计。
如果应用容器线程数远大于连接池大小,就会出现:
经验原则:
maxThreads 不应无限放大当数据库已经接近瓶颈时,继续扩容应用实例通常会让问题更糟。
因为每个新实例都可能携带新的连接池预算,最终把数据库进一步压垮。
更正确的策略是:
如果 getConnection() 已经超时,通常意味着系统整体过载。
这时无脑重试相当于给系统再补一刀。
推荐原则:
建议团队建立事务预算,例如:
< 100ms< 300ms100 ~ 500 条一旦事务时间失控,连接池再大也只是延后故障。
连接池经常只是“受害者”。
如果你看到:
active connections 持续高位pending threads 增长要第一时间排查:
容器环境下,连接池治理必须额外考虑实例弹性、资源限制和优雅下线。
最常见的问题不是 Hikari 参数本身,而是以下组合:
最终会导致数据库在扩容时反而被打挂。
一定要按“峰值 Pod 数”规划连接池:
总数据库安全预算 = 所有在线 Pod 池大小之和 + 运维/任务预留
例如:
则单 Pod 理论上限约为:
(180 - 20) / 8 = 20
这比你当前只有 3 个 Pod 时看到的“单 Pod 似乎可以配 50”更接近真实生产。
建议:
readinessProbe 只有在数据库连接可用时才放流量Pod 下线时,如果还有长事务或连接未及时归还,可能出现:
推荐组合:
preStop 增加摘流等待示例:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
连接池不是纯数据库参数,它也影响应用内存和线程行为。
如果容器 CPU 很小,但连接池很大,会出现:
因此要把以下资源一起看:
生产级连接池治理离不开指标。
如果使用 Spring Boot Actuator + Micrometer,Hikari 指标通常会自动暴露,重点关注:
| 指标 | 含义 | 告警价值 |
|---|---|---|
hikaricp.connections.active |
当前活跃连接数 | 判断池是否持续高压 |
hikaricp.connections.idle |
当前空闲连接数 | 判断池是否被打满 |
hikaricp.connections.pending |
等待连接线程数 | 最关键,直接反映排队 |
hikaricp.connections.max |
最大连接数 | 对照 active 看饱和度 |
hikaricp.connections.min |
最小空闲连接数 | 看预热策略是否合理 |
hikaricp.connections.timeout |
获取连接超时次数 | 直接反映故障发生 |
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: always
metrics:
tags:
application: order-service
可以重点做三类告警:
示例思路:
active / max > 0.8 持续 5 分钟
pending > 0 持续 1 分钟
timeout count 在 5 分钟内持续增长
只看应用指标还不够,建议同时关联:
Threads_runningThreads_connected故障排查路径一般应该是:
应用 RT 升高
-> 看 Hikari pending / timeout
-> 看 active 是否打满
-> 看数据库慢查询/锁等待/CPU
-> 看最近发布、批处理、报表任务、流量突增
遇到问题时,可以先问这三个问题:
这能帮助你避免一上来就盲目调大池大小。
某电商订单服务配置如下:
spring:
datasource:
hikari:
maximum-pool-size: 12
minimum-idle: 12
connection-timeout: 30000
max-lifetime: 1800000
部署规模:
问题在于,仅连接池理论总上限就已经是:
12 × 6 = 72
再加上库存服务、营销服务、后台任务、脚本连接,数据库高峰期接近满负荷。
某次大促时,运营临时开启了实时报表查询,同时订单服务新上线一段代码,在事务中调用了优惠券 RPC:
@Transactional
public void placeOrder(...) {
couponClient.lockCoupon(...); // 远程调用
orderMapper.insert(...);
stockMapper.deduct(...);
}
当优惠券服务出现抖动后:
connectionTimeout=30s,大量 Tomcat 线程长时间阻塞修复不是简单改一个参数,而是组合治理:
connectionTimeout 从 30000ms 降到 2000mspending > 0这个案例说明:
连接池问题几乎总是架构问题、事务问题、容量问题的综合体现。
错误。
大池只是把更多并发直接打到数据库,不会自动提升吞吐。
connectionTimeout 越长越稳错误。
过长等待会制造线程堆积,把局部故障扩散成全局故障。
不一定。
更多时候是:
省事,但风险极高。
核心业务和非核心业务共享池,通常是线上事故的起点。
连接池是应用和数据库的交界层。
只看任意一边都容易误判。
这会制造噪音和额外成本。
更适合作为排障工具,而不是长期暴力监控手段。
maximumPoolSize 先按数据库预算规划,再通过压测校准connectionTimeout 建议控制在 0.5s ~ 5smaxLifetime 小于数据库/NAT 空闲断开时间keepaliveTimeminimumIdle 预热到满池active、idle、pending、timeoutpending > 0 建立敏感告警Spring Boot 中使用 HikariCP 的最佳实践,绝不是“抄一份参数模板”那么简单。真正成熟的连接池治理,至少要同时覆盖五件事:
如果把一句话作为本文结论,那就是:
HikariCP 的正确使用方式,不是把连接池调大,而是让每一个连接都以最短时间、最稳定方式服务真正重要的请求。
当你把连接池放回到整个系统架构中去思考,它就不再只是一个 JDBC 组件,而是数据库稳定性的前线控制面。
相关技术探讨与深入交流,欢迎访问云栈社区。