很多人都遇到过这种诡异现象:本地测试跑得好好的应用,一上线就报"连接超时"或"too many connections";数据库 CPU 不高、磁盘也不忙,但接口响应就是慢;压测加到几百并发,数据库直接拒绝新连接。这些问题十有八九都跟数据库连接池有关——它是应用和数据库之间那道被忽视的阀门。
这篇文章把连接池讲透:为什么需要它、主流连接池(HikariCP、Druid)的关键参数怎么设、池大小到底开多大才好、连接泄漏长什么样、以及连接池和数据库 max_connections 之间必须协调好的关系。读完你能自己算出一个合理的池大小,并能快速定位连接相关的故障。
一、为什么需要连接池
先看没有连接池会怎样。每次访问数据库都新建连接,意味着要经历 TCP 三次握手、身份认证、初始化会话等一整套过程。在 MySQL 上,建立一个连接的开销在毫秒级,高并发下这个开销会被放大成明显的延迟,数据库也会被大量短连接拖垮。
连接池的核心思想是复用:预先建立一批连接放在池子里,应用要用时从池里取,用完还回去,而不是每次新建和关闭。好处有三个:
- 降低延迟:省去建连开销,请求直接拿到可用连接。
- 控制并发:池大小就是应用能同时使用数据库连接数的上限,天然形成保护,避免把数据库打挂。
- 集中管理:超时、健康检查、失败重试、监控指标都能在池这一层统一处理。
代价是连接池本身需要正确配置,配错了反而制造问题:池太小成为瓶颈,池太大压垮数据库,超时没设好导致请求长时间挂起。
一个必须澄清的认知:连接池不是越大越好。这跟直觉相反,下面会专门讲。
二、主流连接池参数解读
Java 生态里最常见的两个连接池是 HikariCP 和 Druid。HikariCP 性能极佳、配置简洁,是 Spring Boot 2.x 之后的默认选择;Druid 功能丰富,带监控面板和 SQL 防火墙,在国内使用广泛。
HikariCP 关键参数
# Spring Boot application.yml 示例
spring:
datasource:
hikari:
maximum-pool-size: 20 # 池内最大连接数,核心参数
minimum-idle: 10 # 最小空闲连接数
connection-timeout: 30000 # 从池获取连接的最长等待时间(毫秒)
idle-timeout: 600000 # 连接空闲超过该时间被回收(毫秒)
max-lifetime: 1800000 # 连接最长存活时间(毫秒),默认 30 分钟
connection-test-query: SELECT 1
pool-name: MyHikariPool
逐个解释:
maximum-pool-size:池的上限,也是应用并发用库的天花板。这是最重要的参数。minimum-idle:池里保持的最小空闲连接数。一般与maximum-pool-size相等,让池保持"热度",避免流量突增时临时建连。connection-timeout:拿不到连接时最多等多久就抛异常。设太长,请求会一直挂着;设太短,正常排队也会失败。这个参数一定要小于上游的 HTTP 超时或 RPC 超时,否则连接池还没超时,调用方已经放弃了。idle-timeout:空闲连接多久没被用就回收。注意它必须小于数据库侧的连接空闲超时(如 MySQL 的wait_timeout)。max-lifetime:连接最长寿命,到期强制关闭重建。这个参数必须小于数据库的 wait_timeout 和基础设施(如 LB、防火墙)的空闲断连时间,否则会用到一个已被服务端关闭的"死连接",报出经典的通信异常。
Druid 关键参数
spring:
datasource:
druid:
initial-size: 5 # 初始化连接数
min-idle: 5 # 最小空闲连接
max-active: 20 # 最大活跃连接(等价于 maximum-pool-size)
max-wait: 3000 # 获取连接最大等待时间(毫秒)
validation-query: SELECT 1
test-while-idle: true # 空闲时检测连接有效性
time-between-eviction-runs-millis: 60000 # 检测间隔
min-evictable-idle-time-millis: 300000 # 连接最小空闲时间
keep-alive: true # 保活,减少被服务端断连
remove-abandoned: true # 回收泄漏连接(慎用,见下文)
remove-abandoned-timeout: 1800 # 超过该秒数未归还则回收
Druid 独有的 remove-abandoned 可以强制回收长时间未归还的连接,看似能治连接泄漏,但它是事后补救,且会掩盖真正的泄漏问题,生产环境应优先修代码而不是依赖它。
参数对照表
| 作用 | HikariCP | Druid |
|---|---|---|
| 最大连接数 | maximum-pool-size | max-active |
| 最小空闲 | minimum-idle | min-idle |
| 获取连接超时 | connection-timeout | max-wait |
| 连接最大寿命 | max-lifetime | phy-timeout(近似) |
| 空闲超时 | idle-timeout | min-evictable-idle-time-millis |
| 有效性检测 | connection-test-query | validation-query + test-while-idle |
三、池大小到底设多少
这是最常被问也最容易设错的地方。先说结论:池大小不是越大越好,理论上并不需要等于并发用户数。
很多人的直觉是"1000 个并发就开 1000 个连接",但这样会把数据库压垮。数据库处理每个连接都要占用内存、CPU、以及上下文切换开销;连接数超过 CPU 能高效处理的量后,吞吐不升反降,因为大量时间花在线程切换和锁竞争上。
一个被广泛引用的经验公式来自 PostgreSQL 社区的实践(同样适用于其他数据库的思考方式):
连接数的合理量级 ≈ CPU 核心数 × 2 + 有效磁盘数
比如一台 8 核的数据库服务器,合理连接数在 20 到 32 左右,而不是几百上千。当然这是"同时活跃执行 SQL"的连接数,不是"应用持有的连接数"。这个公式的价值在于提醒你:真正决定吞吐的是数据库的处理能力,不是连接数量。
更实际的做法是按下面的步骤估:
第一步,估算单个连接每秒能执行多少条 SQL(用压测得到,经验上简单查询单连接每秒几百到上千次)。
第二步,用目标 QPS 除以单连接处理能力,得到需要的活跃连接数。
第三步,考虑突发和波动,乘以 1.5 到 2 倍作为池大小。
第四步,多个应用实例共享同一个数据库时,要把所有实例的池大小加起来,再和数据库 max_connections 比较。
举例:目标 QPS 5000,单连接每秒能跑 500 条简单查询,则需 10 个活跃连接;留余量设 20。如果应用有 4 个实例,总连接需求就是 80。
四、池大小与数据库 max_connections 的关系
这一步必须算清楚,否则上线必炸。
数据库有自己的最大连接数限制:MySQL 是 max_connections(默认 151),PostgreSQL 是 max_connections(默认 100)。应用侧的连接池总和不能超过这个值,而且要给运维、监控、备份、复制等预留一部分。
关键公式:
所有应用实例的池大小之和 + 数据库保留连接 < 数据库 max_connections
举例:有 4 个应用实例,每个 maximum-pool-size=20,总计 80;再加监控、备份、DBA 手工连接约 10 个,总需求 90。那 MySQL 的 max_connections 就要设到 120 以上,留出安全余量。
-- MySQL:查看和设置
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW STATUS LIKE 'Max_used_connections'; -- 历史峰值
SET GLOBAL max_connections = 300; -- 临时生效,重启失效
-- PostgreSQL:
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity; -- 当前连接数
注意 max_connections 是数据库实例全局的,不是每库或每用户。改大它也有代价:MySQL 每个连接占用一定内存,PostgreSQL 每个连接是一个进程(更重),盲目调大可能触发内存不足。PostgreSQL 更适合配连接池中间件(PgBouncer)来支撑高并发。
还有一个常被忽略的点:数据库侧的空闲连接超时和连接池的寿命参数要对齐。
- MySQL 的
wait_timeout(默认 28800 秒即 8 小时)控制空闲连接多久被服务端关闭。 - 连接池的
max-lifetime必须小于wait_timeout,否则池里会留着已被服务端关闭的连接,取出来用时直接报错。
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
经验做法:把 MySQL 的 wait_timeout 调到 600 到 1800 秒之间,连接池 max-lifetime 设得更小一点(如 1200 秒),并开启连接的保活检测。
五、超时设置怎么调
超时是连接池里最需要"分层设计"的部分。原则是:内层超时 < 外层超时,形成从内到外逐层放大的阶梯。
一个典型的超时链路:
| 层级 | 参数 | 参考值 |
|---|---|---|
| SQL 执行 | statement timeout / query timeout | 3~10 秒 |
| 获取连接 | connection-timeout / max-wait | 1~3 秒 |
| 数据库连接 | connect timeout / socket timeout | 3~5 秒 |
| RPC/HTTP 调用 | 调用超时 | 5~10 秒 |
| 网关/负载均衡 | 请求超时 | 10~30 秒 |
为什么内层要更短?因为如果内层超时比外层还长,外层已经超时并给用户返回失败了,内层还在占着线程和连接慢慢跑,白白浪费资源,还可能引起连锁的线程池耗尽。
connection-timeout 的取值要特别小心。它表示"从池里拿连接最多等多久"。如果设成 30 秒,那么在数据库慢的时候,请求会在连接池这一层堆积 30 秒,形成雪崩。一般设 1 到 3 秒,快速失败、快速释放,反而更健康。
六、连接泄漏:最常见的隐形杀手
连接泄漏指的是应用从池里拿了连接却没有归还(通常是异常路径下漏了 close())。泄漏的后果是池里的连接被慢慢耗光,应用开始大面积超时,而数据库侧看连接数却不高(因为池已耗尽,无法新建)。这是排查时最容易困惑的地方。
泄漏的典型表现:
- 运行一段时间后(不是立刻)开始报"获取连接超时"。
- 重启应用后恢复正常,过一段时间又出问题。
- 数据库
Threads_connected或pg_stat_activity里看到连接一直挂在那里、状态是 Sleep/Idle,且长时间不释放。 - 连接池监控里活跃连接数只增不减。
HikariCP 有个很好的特性:当连接泄漏时,日志里会打印获取该连接时的堆栈:
Connection leak detection triggered for ... on thread ...
java.lang.Exception: Apparent connection leak detected
at ...
看到这个堆栈,顺着代码找到没关闭连接的地方即可。用 try-with-resources 或者框架的模板方法(JdbcTemplate、MyBatis 由框架管理)是避免泄漏的根本办法。
数据库侧也可以辅助排查长连接:
-- MySQL:找出长时间空闲或运行中的连接
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 600
ORDER BY time DESC;
-- PostgreSQL:找空闲时间过长的连接
SELECT pid, usename, application_name, state, now() - state_change AS idle_time, query
FROM pg_stat_activity
WHERE state = 'idle'
ORDER BY state_change;
风险提示:不要用
KILL或pg_terminate_backend去批量杀连接来"解决"泄漏,尤其不要杀正在执行重要写操作的连接,可能导致事务异常、锁残留、甚至数据不一致。正确做法是修应用代码,杀连接只用于应急止血。
七、性能优化 checklist
把连接池相关的优化要点整理成一份清单:
- 池大小按"数据库处理能力"而非"并发用户数"来定,从
CPU × 2 + 磁盘数这个经验量级出发压测确认。 - 所有实例池大小之和 加保留量小于数据库
max_connections,并核对Threads_connected的历史峰值。 max-lifetime< 数据库wait_timeout和其他空闲断连时间,避免取到死连接。connection-timeout设短(1~3 秒),并保证它小于外层超时,避免连接池层堆积。- 开启连接有效性检测(test-while-idle 或 connection-test-query),及时剔除坏连接。
- 用掉获取连接超时的日志和池监控指标(活跃数、空闲数、等待数、等待时间)做日常告警。
- SQL 层要配 statement timeout,防止单条慢 SQL 长期占用连接。
- 不要在事务里做远程调用,否则连接会被长时间占住,池再大也不够。
监控指标上,HikariCP 通过 Micrometer 暴露 hikaricp_connections_active、hikaricp_connections_idle、hikaricp_connections_pending、hikaricp_connections_acquire_seconds 等,Druid 有内置的 StatFilter 和监控页。重点看等待获取连接的线程数(pending/等待线程)——只要它经常不为零,就说明池可能偏小或存在慢查询占连接。
常见问题
Q:为什么我配了很大的池,反而更慢了?
数据库连接数过多会导致 CPU 上下文切换、锁竞争、内存压力上升,吞吐不升反降。连接数应匹配数据库的处理能力,而不是并发用户数。
Q:报错 "too many connections" 怎么处理?
先确认是哪个层报的。如果是 MySQL 返回的,说明数据库的 max_connections 被打满了,检查所有实例的池大小之和。如果应用侧报的是获取连接超时,那是应用池耗尽,排查泄漏或慢 SQL。
Q:max-lifetime 为什么要小于 wait_timeout?
否则连接池会保留已经被服务端关闭的连接,取出来用时报通信异常(如 Communications link failure)。让池主动、提前地把老连接换掉,才是安全的。
Q:空闲连接数一直很高是问题吗?
不一定。如果 minimum-idle 设得高,空闲连接多属正常。但如果空闲连接很多而等待队列也长,说明池分配不均或有慢 SQL 占着活跃连接不放。
Q:连接池和 PgBouncer 这类中间件能一起用吗?
可以,而且 PostgreSQL 高并发场景推荐这样做。PgBouncer 做一层连接复用,应用侧的池可以设得更小。注意 PgBouncer 的两种模式(session/transaction)对事务和会话级特性支持不同,要按需选。
总结
- 连接池的核心是复用连接、控制并发,但它不是越大越好,池大小要按数据库处理能力来定。
- 关键参数是最大池大小、获取连接超时、连接最大寿命和空闲超时,其中
max-lifetime必须小于数据库的wait_timeout。 - 所有应用实例的池大小之和加上保留量,必须小于数据库
max_connections,并且要留监控、备份、DBA 的空间。 - 超时要分层设计,内层短于外层,
connection-timeout设 1 到 3 秒快速失败,避免雪崩。 - 连接泄漏是隐形杀手,靠 HikariCP 的泄漏日志和数据库长连接查询定位,从代码修复,别动不动杀连接。
- 日常监控活跃、空闲、等待连接数和获取连接耗时,等待数长期非零就要调整。
企业QQ咨询




