数据库连接池原理与HikariCP调优实战

前言

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

查了半天发现是数据库连接的锅------每次请求都新建连接,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
+--------------------+
|    应用代码        |
+--------------------+

好处:

  1. 避免重复建连接:几十毫秒 → 微秒级
  2. 控制连接数量:防止撑爆数据库
  3. 连接复用:一个连接可以被多个请求复用

连接池核心原理

核心数据结构

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

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快

  1. 字节码优化:用Javassist动态生成代理类,比反射快
  2. 无锁设计:用CAS代替synchronized,减少线程阻塞
  3. FastList:自定义的List实现,针对连接池场景优化
  4. 精简代码:整个核心代码只有几千行

基本配置

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;

可能原因:

  1. 连接池太小
  2. 有慢查询占着连接不放
  3. 连接泄漏(借出去没还)

问题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:多机房注意网络

我们有跨机房数据库访问的场景,连接建立延迟高。这种情况下:

  1. 适当增大 connection-timeout
  2. 用更激进的预热策略
  3. 我们用星空组网把两边网络打通后,延迟稳定很多

总结

连接池的核心就三件事:

功能 配置项 建议值
池大小 maximum-pool-size CPU核数 * 2 ~ 4
超时控制 connection-timeout 30秒
连接存活 max-lifetime < 数据库wait_timeout

记住几个原则:

  1. 连接数不是越多越好,够用就行
  2. 监控比调参重要,先能看到问题
  3. 连接泄漏是大忌,用框架自动管理
  4. 超时要协调,连接池和数据库要配合

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

相关推荐
廋到被风吹走2 小时前
【Spring】Spring ORM 深度解析
java·后端·spring
自由生长20242 小时前
系统的雪崩-反脆弱设计
后端
卜锦元2 小时前
Golang后端性能优化手册(第二章:缓存策略与优化)
开发语言·数据库·后端·性能优化·golang
掘金酱2 小时前
🏆2025 AI/Vibe Coding 对我的影响 | 年终技术征文
前端·人工智能·后端
狗头大军之江苏分军2 小时前
2026年了,前端到底算不算“夕阳行业”?
前端·javascript·后端
宋情写2 小时前
Springboot基础篇01-创建一个SpringBoot项目
java·spring boot·后端
今夕资源网2 小时前
go-tcnat内网端口映射 端口穿透 GO语言 免费开源
开发语言·后端·golang·go语言·端口映射·内网端口映射
踏浪无痕2 小时前
一个 Java 老兵转 Go 后,终于理解了“简单”的力量
后端·程序员·go
汪凝同学要努力2 小时前
依赖注入 - Spring 在 IoC 容器里查找一个 Bean 的不同方式示例
后端