前言
刚工作那会儿,遇到过一个诡异的问题:服务刚启动时第一批请求特别慢,好几秒才响应,之后就正常了。
查了半天发现是数据库连接的锅------每次请求都新建连接,TCP握手 + MySQL认证,一套下来几百毫秒。用上连接池后,响应时间从秒级降到毫秒级。
连接池这东西,平时不出问题感觉不到它的存在,一出问题就是大麻烦。这篇文章讲清楚原理和调优,让你以后遇到问题能快速定位。
为什么需要连接池
创建连接的代价
数据库连接不是直接就能用的,要经过:
lua
客户端 数据库
| |
|------- SYN ---------> |
|<------ SYN-ACK ------ | TCP三次握手
|------- ACK ---------> |
| |
|---- 认证请求 --------> |
|<--- 认证Challenge ---- | MySQL认证
|---- 认证响应 --------> |
|<--- 认证OK ---------- |
| |
一次连接建立,最少也要几十毫秒(局域网),跨机房可能几百毫秒。
如果每次查询都新建连接:
java
// 不用连接池(千万别这样写)
public User getUser(int id) {
Connection conn = DriverManager.getConnection(url, user, password); // 每次新建
try {
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setInt(1, id);
ResultSet rs = ps.executeQuery();
// ...
} finally {
conn.close(); // 用完就关
}
}
假设100 QPS,每个连接建立耗时50ms,光建连接就得5秒。
连接池的作用
连接池预先创建好一批连接,要用的时候借出去,用完还回来:
lua
+--------------------+
| 连接池 |
| +----+ +----+ |
| |conn| |conn| ... | <- 空闲连接
| +----+ +----+ |
+--------------------+
^ |
| v
还回 借出
^ |
| v
+--------------------+
| 应用代码 |
+--------------------+
好处:
- 避免重复建连接:几十毫秒 → 微秒级
- 控制连接数量:防止撑爆数据库
- 连接复用:一个连接可以被多个请求复用
连接池核心原理
核心数据结构
一个连接池至少要有这些东西:
java
class SimpleConnectionPool {
// 空闲连接队列
private Queue<Connection> idleConnections = new LinkedList<>();
// 正在使用的连接
private Set<Connection> activeConnections = new HashSet<>();
// 最大连接数
private int maxPoolSize = 10;
// 等待获取连接的线程
private Object lock = new Object();
}
获取连接的流程
java
public Connection getConnection(long timeout) {
synchronized (lock) {
long deadline = System.currentTimeMillis() + timeout;
while (true) {
// 1. 有空闲连接,直接返回
if (!idleConnections.isEmpty()) {
Connection conn = idleConnections.poll();
activeConnections.add(conn);
return conn;
}
// 2. 没达到最大数,创建新连接
if (activeConnections.size() < maxPoolSize) {
Connection conn = createNewConnection();
activeConnections.add(conn);
return conn;
}
// 3. 等待其他线程归还
long waitTime = deadline - System.currentTimeMillis();
if (waitTime <= 0) {
throw new SQLException("获取连接超时");
}
lock.wait(waitTime);
}
}
}
归还连接
java
public void returnConnection(Connection conn) {
synchronized (lock) {
activeConnections.remove(conn);
// 检查连接是否还有效
if (isValid(conn)) {
idleConnections.offer(conn);
} else {
conn.close(); // 坏了就丢掉
}
lock.notifyAll(); // 唤醒等待的线程
}
}
连接有效性检测
连接可能因为各种原因失效:
- 数据库重启
- 网络中断
- 连接超时被踢
所以借出前要检测:
java
private boolean isValid(Connection conn) {
try {
// 方式1:发送轻量查询
Statement stmt = conn.createStatement();
stmt.execute("SELECT 1");
return true;
} catch (SQLException e) {
return false;
}
}
HikariCP:最快的连接池
HikariCP是目前最快的Java连接池,SpringBoot 2.x默认用它。
为什么HikariCP快
- 字节码优化:用Javassist动态生成代理类,比反射快
- 无锁设计:用CAS代替synchronized,减少线程阻塞
- FastList:自定义的List实现,针对连接池场景优化
- 精简代码:整个核心代码只有几千行
基本配置
yaml
# application.yml
spring:
datasource:
hikari:
# 连接池大小
minimum-idle: 5 # 最小空闲连接
maximum-pool-size: 20 # 最大连接数
# 超时设置
connection-timeout: 30000 # 获取连接超时(毫秒)
idle-timeout: 600000 # 空闲连接超时(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
# 连接检测
connection-test-query: SELECT 1
连接池大小怎么设
这是最常被问的问题。官方有个公式:
scss
连接数 = (核心数 * 2) + 有效磁盘数
但实际情况要复杂得多,建议从小开始逐步调整:
makefile
# 一般Web应用
最大连接数 = CPU核数 * 2 ~ 4
# 例如8核服务器
maximum-pool-size: 20
minimum-idle: 5
为什么不是越大越好?
连接数太多:
├── 数据库连接数有限(MySQL默认151)
├── 每个连接都占内存(MySQL每连接约1MB)
├── 更多连接 = 更多上下文切换
└── 锁竞争更激烈
经验法则:宁可排队等连接,不要撑爆数据库。
超时参数详解
yaml
hikari:
# 获取连接最多等30秒
connection-timeout: 30000
# 空闲连接超过10分钟就关闭
# 注意:要小于数据库的wait_timeout
idle-timeout: 600000
# 连接最多存活30分钟,然后强制关闭重建
# 防止连接时间太长出问题
max-lifetime: 1800000
关键点 :max-lifetime 必须比数据库的 wait_timeout 小几分钟:
sql
-- 查看MySQL的wait_timeout
SHOW VARIABLES LIKE 'wait_timeout';
-- 默认28800秒(8小时)
如果 max-lifetime > wait_timeout,数据库会先把连接断掉,连接池不知道,就会拿到死连接。
监控指标
HikariCP暴露了很多指标,配合Prometheus很好用:
java
// 开启指标
HikariConfig config = new HikariConfig();
config.setMetricRegistry(new PrometheusMeterRegistry(...));
关键指标:
promql
# 活跃连接数
hikaricp_connections_active
# 空闲连接数
hikaricp_connections_idle
# 等待获取连接的线程数
hikaricp_connections_pending
# 获取连接耗时
hikaricp_connections_acquire_seconds
告警规则:
yaml
groups:
- name: hikari-alerts
rules:
- alert: ConnectionPoolExhausted
expr: hikaricp_connections_pending > 0
for: 1m
annotations:
summary: "连接池耗尽,有线程在等待"
- alert: ConnectionAcquireSlow
expr: hikaricp_connections_acquire_seconds_max > 1
for: 5m
annotations:
summary: "获取连接超过1秒"
常见问题排查
问题1:Connection is not available
csharp
HikariPool-1 - Connection is not available, request timed out after 30000ms
原因:连接池满了,30秒内没拿到连接。
排查:
sql
-- 看数据库实际连接数
SHOW STATUS LIKE 'Threads_connected';
-- 看连接来源
SELECT * FROM information_schema.processlist;
可能原因:
- 连接池太小
- 有慢查询占着连接不放
- 连接泄漏(借出去没还)
问题2:连接泄漏
连接借出去忘了还,池子里的连接越来越少。
HikariCP有泄漏检测:
yaml
hikari:
leak-detection-threshold: 60000 # 连接借出超过60秒就报警
日志会显示借出连接的堆栈,定位泄漏代码:
less
ProxyLeakTask - Connection leak detection triggered for conn0
at com.example.UserService.getUser(UserService.java:42)
at ...
问题3:连接被数据库断开
perl
Communications link failure
The last packet successfully received from the server was xxx milliseconds ago
原因:连接闲置太久,被数据库踢了。
解决:
yaml
hikari:
# 定期发心跳保活
keepalive-time: 30000 # 每30秒发一次心跳
# 或者让连接在数据库踢之前主动关闭
max-lifetime: 1700000 # 小于wait_timeout
问题4:启动时连接失败
服务启动时数据库还没好,连接失败:
yaml
hikari:
# 初始化时允许失败
initialization-fail-timeout: -1
# 慢慢重试
connection-timeout: 30000
或者用优雅启动,等数据库好了再接入流量。
多数据源配置
实际项目经常要连多个库:
java
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary.hikari")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary.hikari")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
yaml
spring:
datasource:
primary:
hikari:
jdbc-url: jdbc:mysql://master:3306/db
maximum-pool-size: 20
secondary:
hikari:
jdbc-url: jdbc:mysql://slave:3306/db
maximum-pool-size: 10
读写分离
java
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "slave" : "master";
}
}
生产经验
经验1:监控先行
上线前先把监控加上。连接池问题往往是间歇性的,没监控数据很难定位。
经验2:压测确定参数
不同业务对连接池需求不同。用压测工具(JMeter、wrk)在真实负载下调整参数。
经验3:分环境配置
yaml
# 开发环境
spring:
profiles: dev
datasource:
hikari:
maximum-pool-size: 5
# 生产环境
spring:
profiles: prod
datasource:
hikari:
maximum-pool-size: 30
经验4:多机房注意网络
我们有跨机房数据库访问的场景,连接建立延迟高。这种情况下:
- 适当增大
connection-timeout - 用更激进的预热策略
- 我们用星空组网把两边网络打通后,延迟稳定很多
总结
连接池的核心就三件事:
| 功能 | 配置项 | 建议值 |
|---|---|---|
| 池大小 | maximum-pool-size | CPU核数 * 2 ~ 4 |
| 超时控制 | connection-timeout | 30秒 |
| 连接存活 | max-lifetime | < 数据库wait_timeout |
记住几个原则:
- 连接数不是越多越好,够用就行
- 监控比调参重要,先能看到问题
- 连接泄漏是大忌,用框架自动管理
- 超时要协调,连接池和数据库要配合
连接池配好了是透明的,配不好就是定时炸弹。希望这篇文章能帮你理解原理,遇到问题时知道往哪个方向排查。