前言
刚工作那会儿,遇到过一个诡异的问题:服务刚启动时第一批请求特别慢,好几秒才响应,之后就正常了。
查了半天发现是数据库连接的锅——每次请求都新建连接,TCP握手 + MySQL认证,一套下来几百毫秒。用上连接池后,响应时间从秒级降到毫秒级。
连接池这东西,平时不出问题感觉不到它的存在,一出问题就是大麻烦。这篇文章讲清楚原理和调优,让你以后遇到问题能快速定位。
为什么需要连接池
创建连接的代价
数据库连接不是直接就能用的,要经过:
客户端 数据库 | | |------- SYN ---------> | |<------ SYN-ACK ------ | TCP三次握手 |------- ACK ---------> | | | |---- 认证请求 --------> | |<--- 认证Challenge ---- | MySQL认证 |---- 认证响应 --------> | |<--- 认证OK ---------- | | |一次连接建立,最少也要几十毫秒(局域网),跨机房可能几百毫秒。
如果每次查询都新建连接:
// 不用连接池(千万别这样写)publicUsergetUser(intid){Connectionconn=DriverManager.getConnection(url,user,password);// 每次新建try{PreparedStatementps=conn.prepareStatement("SELECT * FROM user WHERE id = ?");ps.setInt(1,id);ResultSetrs=ps.executeQuery();// ...}finally{conn.close();// 用完就关}}假设100 QPS,每个连接建立耗时50ms,光建连接就得5秒。
连接池的作用
连接池预先创建好一批连接,要用的时候借出去,用完还回来:
+--------------------+ | 连接池 | | +----+ +----+ | | |conn| |conn| ... | <- 空闲连接 | +----+ +----+ | +--------------------+ ^ | | v 还回 借出 ^ | | v +--------------------+ | 应用代码 | +--------------------+好处:
- 避免重复建连接:几十毫秒 → 微秒级
- 控制连接数量:防止撑爆数据库
- 连接复用:一个连接可以被多个请求复用
连接池核心原理
核心数据结构
一个连接池至少要有这些东西:
classSimpleConnectionPool{// 空闲连接队列privateQueue<Connection>idleConnections=newLinkedList<>();// 正在使用的连接privateSet<Connection>activeConnections=newHashSet<>();// 最大连接数privateintmaxPoolSize=10;// 等待获取连接的线程privateObjectlock=newObject();}获取连接的流程
publicConnectiongetConnection(longtimeout){synchronized(lock){longdeadline=System.currentTimeMillis()+timeout;while(true){// 1. 有空闲连接,直接返回if(!idleConnections.isEmpty()){Connectionconn=idleConnections.poll();activeConnections.add(conn);returnconn;}// 2. 没达到最大数,创建新连接if(activeConnections.size()<maxPoolSize){Connectionconn=createNewConnection();activeConnections.add(conn);returnconn;}// 3. 等待其他线程归还longwaitTime=deadline-System.currentTimeMillis();if(waitTime<=0){thrownewSQLException("获取连接超时");}lock.wait(waitTime);}}}归还连接
publicvoidreturnConnection(Connectionconn){synchronized(lock){activeConnections.remove(conn);// 检查连接是否还有效if(isValid(conn)){idleConnections.offer(conn);}else{conn.close();// 坏了就丢掉}lock.notifyAll();// 唤醒等待的线程}}连接有效性检测
连接可能因为各种原因失效:
- 数据库重启
- 网络中断
- 连接超时被踢
所以借出前要检测:
privatebooleanisValid(Connectionconn){try{// 方式1:发送轻量查询Statementstmt=conn.createStatement();stmt.execute("SELECT 1");returntrue;}catch(SQLExceptione){returnfalse;}}HikariCP:最快的连接池
HikariCP是目前最快的Java连接池,SpringBoot 2.x默认用它。
为什么HikariCP快
- 字节码优化:用Javassist动态生成代理类,比反射快
- 无锁设计:用CAS代替synchronized,减少线程阻塞
- FastList:自定义的List实现,针对连接池场景优化
- 精简代码:整个核心代码只有几千行
基本配置
# application.ymlspring: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_timeoutidle-timeout:600000# 连接最多存活30分钟,然后强制关闭重建# 防止连接时间太长出问题max-lifetime:1800000关键点:max-lifetime必须比数据库的wait_timeout小几分钟:
-- 查看MySQL的wait_timeoutSHOWVARIABLESLIKE'wait_timeout';-- 默认28800秒(8小时)如果max-lifetime > wait_timeout,数据库会先把连接断掉,连接池不知道,就会拿到死连接。
监控指标
HikariCP暴露了很多指标,配合Prometheus很好用:
// 开启指标HikariConfigconfig=newHikariConfig();config.setMetricRegistry(newPrometheusMeterRegistry(...));关键指标:
# 活跃连接数 hikaricp_connections_active # 空闲连接数 hikaricp_connections_idle # 等待获取连接的线程数 hikaricp_connections_pending # 获取连接耗时 hikaricp_connections_acquire_seconds告警规则:
groups:-name:hikari-alertsrules:-alert:ConnectionPoolExhaustedexpr:hikaricp_connections_pending>0for:1mannotations:summary:"连接池耗尽,有线程在等待"-alert:ConnectionAcquireSlowexpr:hikaricp_connections_acquire_seconds_max>1for:5mannotations:summary:"获取连接超过1秒"常见问题排查
问题1:Connection is not available
HikariPool-1 - Connection is not available, request timed out after 30000ms原因:连接池满了,30秒内没拿到连接。
排查:
-- 看数据库实际连接数SHOWSTATUSLIKE'Threads_connected';-- 看连接来源SELECT*FROMinformation_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或者用优雅启动,等数据库好了再接入流量。
多数据源配置
实际项目经常要连多个库:
@ConfigurationpublicclassDataSourceConfig{@Bean@Primary@ConfigurationProperties("spring.datasource.primary.hikari")publicDataSourceprimaryDataSource(){returnDataSourceBuilder.create().type(HikariDataSource.class).build();}@Bean@ConfigurationProperties("spring.datasource.secondary.hikari")publicDataSourcesecondaryDataSource(){returnDataSourceBuilder.create().type(HikariDataSource.class).build();}}spring:datasource:primary:hikari:jdbc-url:jdbc:mysql://master:3306/dbmaximum-pool-size:20secondary:hikari:jdbc-url:jdbc:mysql://slave:3306/dbmaximum-pool-size:10读写分离
publicclassRoutingDataSourceextendsAbstractRoutingDataSource{@OverrideprotectedObjectdetermineCurrentLookupKey(){returnTransactionSynchronizationManager.isCurrentTransactionReadOnly()?"slave":"master";}}生产经验
经验1:监控先行
上线前先把监控加上。连接池问题往往是间歇性的,没监控数据很难定位。
经验2:压测确定参数
不同业务对连接池需求不同。用压测工具(JMeter、wrk)在真实负载下调整参数。
经验3:分环境配置
# 开发环境spring:profiles:devdatasource:hikari:maximum-pool-size:5# 生产环境spring:profiles:proddatasource:hikari:maximum-pool-size:30经验4:多机房注意网络
我们有跨机房数据库访问的场景,连接建立延迟高。这种情况下:
- 适当增大
connection-timeout - 用更激进的预热策略
- 我们用星空组网把两边网络打通后,延迟稳定很多
总结
连接池的核心就三件事:
| 功能 | 配置项 | 建议值 |
|---|---|---|
| 池大小 | maximum-pool-size | CPU核数 * 2 ~ 4 |
| 超时控制 | connection-timeout | 30秒 |
| 连接存活 | max-lifetime | < 数据库wait_timeout |
记住几个原则:
- 连接数不是越多越好,够用就行
- 监控比调参重要,先能看到问题
- 连接泄漏是大忌,用框架自动管理
- 超时要协调,连接池和数据库要配合
连接池配好了是透明的,配不好就是定时炸弹。希望这篇文章能帮你理解原理,遇到问题时知道往哪个方向排查。