前言

刚工作那会儿,遇到过一个诡异的问题:服务刚启动时第一批请求特别慢,好几秒才响应,之后就正常了。

查了半天发现是数据库连接的锅——每次请求都新建连接,TCP握手 + MySQL认证,一套下来几百毫秒。用上连接池后,响应时间从秒级降到毫秒级。

连接池这东西,平时不出问题感觉不到它的存在,一出问题就是大麻烦。这篇文章讲清楚原理和调优,让你以后遇到问题能快速定位。

为什么需要连接池

创建连接的代价

数据库连接不是直接就能用的,要经过:

客户端 数据库

| |

|------- SYN ---------> |

|<------ SYN-ACK ------ | TCP三次握手

|------- ACK ---------> |

| |

|---- 认证请求 --------> |

|<--- 认证Challenge ---- | MySQL认证

|---- 认证响应 --------> |

|<--- 认证OK ---------- |

| |

一次连接建立,最少也要几十毫秒(局域网),跨机房可能几百毫秒。

如果每次查询都新建连接:

// 不用连接池(千万别这样写)

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秒。

连接池的作用

连接池预先创建好一批连接,要用的时候借出去,用完还回来:

+--------------------+

| 连接池 |

| +----+ +----+ |

| |conn| |conn| ... | <- 空闲连接

| +----+ +----+ |

+--------------------+

^ |

| v

还回 借出

^ |

| v

+--------------------+

| 应用代码 |

+--------------------+

好处:

避免重复建连接:几十毫秒 → 微秒级

控制连接数量:防止撑爆数据库

连接复用:一个连接可以被多个请求复用

连接池核心原理

核心数据结构

一个连接池至少要有这些东西:

class SimpleConnectionPool {

// 空闲连接队列

private Queue idleConnections = new LinkedList<>();

// 正在使用的连接

private Set activeConnections = new HashSet<>();

// 最大连接数

private int maxPoolSize = 10;

// 等待获取连接的线程

private Object lock = new Object();

}

获取连接的流程

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);

}

}

}

归还连接

public void returnConnection(Connection conn) {

synchronized (lock) {

activeConnections.remove(conn);

// 检查连接是否还有效

if (isValid(conn)) {

idleConnections.offer(conn);

} else {

conn.close(); // 坏了就丢掉

}

lock.notifyAll(); // 唤醒等待的线程

}

}

连接有效性检测

连接可能因为各种原因失效:

数据库重启

网络中断

连接超时被踢

所以借出前要检测:

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实现,针对连接池场景优化

精简代码:整个核心代码只有几千行

基本配置

# 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

连接池大小怎么设

这是最常被问的问题。官方有个公式:

连接数 = (核心数 * 2) + 有效磁盘数

但实际情况要复杂得多,建议从小开始逐步调整:

# 一般Web应用

最大连接数 = CPU核数 * 2 ~ 4

# 例如8核服务器

maximum-pool-size: 20

minimum-idle: 5

为什么不是越大越好?

连接数太多:

├── 数据库连接数有限(MySQL默认151)

├── 每个连接都占内存(MySQL每连接约1MB)

├── 更多连接 = 更多上下文切换

└── 锁竞争更激烈

经验法则:宁可排队等连接,不要撑爆数据库。

超时参数详解

hikari:

# 获取连接最多等30秒

connection-timeout: 30000

# 空闲连接超过10分钟就关闭

# 注意:要小于数据库的wait_timeout

idle-timeout: 600000

# 连接最多存活30分钟,然后强制关闭重建

# 防止连接时间太长出问题

max-lifetime: 1800000

关键点:max-lifetime 必须比数据库的 wait_timeout 小几分钟:

-- 查看MySQL的wait_timeout

SHOW VARIABLES LIKE 'wait_timeout';

-- 默认28800秒(8小时)

如果 max-lifetime > wait_timeout,数据库会先把连接断掉,连接池不知道,就会拿到死连接。

监控指标

HikariCP暴露了很多指标,配合Prometheus很好用:

// 开启指标

HikariConfig config = new HikariConfig();

config.setMetricRegistry(new PrometheusMeterRegistry(...));

关键指标:

# 活跃连接数

hikaricp_connections_active

# 空闲连接数

hikaricp_connections_idle

# 等待获取连接的线程数

hikaricp_connections_pending

# 获取连接耗时

hikaricp_connections_acquire_seconds

告警规则:

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

HikariPool-1 - Connection is not available, request timed out after 30000ms

原因:连接池满了,30秒内没拿到连接。

排查:

-- 看数据库实际连接数

SHOW STATUS LIKE 'Threads_connected';

-- 看连接来源

SELECT * FROM information_schema.processlist;

可能原因:

连接池太小

有慢查询占着连接不放

连接泄漏(借出去没还)

问题2:连接泄漏

连接借出去忘了还,池子里的连接越来越少。

HikariCP有泄漏检测:

hikari:

leak-detection-threshold: 60000 # 连接借出超过60秒就报警

日志会显示借出连接的堆栈,定位泄漏代码:

ProxyLeakTask - Connection leak detection triggered for conn0

at com.example.UserService.getUser(UserService.java:42)

at ...

问题3:连接被数据库断开

Communications link failure

The last packet successfully received from the server was xxx milliseconds ago

原因:连接闲置太久,被数据库踢了。

解决:

hikari:

# 定期发心跳保活

keepalive-time: 30000 # 每30秒发一次心跳

# 或者让连接在数据库踢之前主动关闭

max-lifetime: 1700000 # 小于wait_timeout

问题4:启动时连接失败

服务启动时数据库还没好,连接失败:

hikari:

# 初始化时允许失败

initialization-fail-timeout: -1

# 慢慢重试

connection-timeout: 30000

或者用优雅启动,等数据库好了再接入流量。

多数据源配置

实际项目经常要连多个库:

@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();

}

}

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

读写分离

public class RoutingDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

return TransactionSynchronizationManager.isCurrentTransactionReadOnly()

? "slave" : "master";

}

}

生产经验

经验1:监控先行

上线前先把监控加上。连接池问题往往是间歇性的,没监控数据很难定位。

经验2:压测确定参数

不同业务对连接池需求不同。用压测工具(JMeter、wrk)在真实负载下调整参数。

经验3:分环境配置

# 开发环境

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

记住几个原则:

连接数不是越多越好,够用就行

监控比调参重要,先能看到问题

连接泄漏是大忌,用框架自动管理

超时要协调,连接池和数据库要配合

连接池配好了是透明的,配不好就是定时炸弹。希望这篇文章能帮你理解原理,遇到问题时知道往哪个方向排查。