MySQL 连接池全场景解析

当 pool size = 100 时,1 个请求 / 100 个请求 / 101 个请求分别会发生什么?

连接池是什么

连接池(Connection Pool)是一组预先创建好的数据库连接的缓存。应用启动时,连接池会初始化一定数量的 TCP 连接到 MySQL,后续请求直接从池中借出连接使用,用完归还,避免反复建立/销毁 TCP 连接的开销。

连接池结构示意 初始状态
100
空闲连接
0
活跃连接
0
等待线程
100
Pool Size
关键参数:连接池的行为由几个核心配置决定——maxPoolSize(最大连接数)、connectionTimeout(获取连接超时时间)、idleTimeout(空闲连接存活时间)、maxLifetime(连接最大存活时间)。不同的连接池实现(HikariCP、Druid、c3p0)参数名称略有不同,但语义一致。

一个请求的完整生命周期

请求到达
业务线程发起 SQL 调用
获取连接
从池中取出空闲连接
执行 SQL
在借出的连接上执行查询
获取结果
读取 ResultSet
归还连接
close() 实际是归还池
关键误区:代码中调用 connection.close() 并不是关闭 TCP 连接!连接池对 Connection 对象做了代理(Proxy),close() 被拦截后实际执行的是将连接状态重置并归还到池中,TCP 连接依然存活。

场景一:1 个请求到来

SCENARIO 01
池中有 100 个空闲连接,1 个请求到来
连接池状态变化 轻松应对
99
空闲连接
1
活跃连接
0
等待线程
100
Pool Size
  1. 1
    请求到达,业务线程调用 dataSource.getConnection()
  2. 2
    池中取连接:连接池发现有空闲连接,直接取出一个分配给该线程。此时空闲连接数变为 99,活跃连接数变为 1
  3. 3
    执行 SQL:业务线程通过这个连接发送 SQL 给 MySQL,MySQL 执行后返回结果
  4. 4
    归还连接:业务代码执行 connection.close(),代理对象拦截此调用,将连接重置状态(清空事务上下文、回滚未提交事务等),放回池的空闲队列。空闲连接数恢复为 100
getConnection() — 立即返回,耗时约 0.1ms(内存操作)
执行 SQL — 网络I/O + MySQL执行,耗时取决于查询复杂度(通常 1ms~100ms)
close() — 归还池,耗时约 0.05ms(重置状态 + 入队)
什么时候释放?连接的"释放"就是 close() 被调用的时刻。在 JDBC 中,标准写法是 try-with-resources 或 finally 块中 close。释放不是销毁 TCP 连接,而是归还到池中。如果代码忘记 close,连接就被泄漏(leak),池中可用连接越来越少,最终耗尽。

场景二:100 个请求同时到来

SCENARIO 02
池中有 100 个空闲连接,100 个请求同时到来
连接池状态 — 全部占满 满载运行
0
空闲连接
100
活跃连接
0
等待线程
100
Pool Size
  1. 1
    100 个请求同时到达,各自调用 getConnection()
  2. 2
    竞争分配:100 个线程从池中抢连接。连接池内部通常用 ConcurrentBag(HikariCP)或锁机制保证线程安全,逐一分配。最终 100 个连接全部分配完毕
  3. 3
    全部在执行:100 个请求各自在自己的连接上执行 SQL,此时空闲连接 = 0,活跃连接 = 100
  4. 4
    逐步归还:先执行完的请求先归还连接。每归还一个,空闲连接 +1。后续如果有新请求到来,可以直接复用
  5. 5
    全部归还:100 个请求全部完成后,连接池恢复到初始状态(空闲 = 100)
注意:虽然 100 个连接都被占满了,但没有请求被阻塞或拒绝。只要请求在 connectionTimeout 之前拿到连接就不会超时。此场景下每个请求都立刻拿到了连接,只是 MySQL 端需要同时处理 100 个查询,可能因锁竞争、CPU 调度等原因导致 SQL 执行变慢。

场景三:101 个请求同时到来

SCENARIO 03
池中有 100 个连接,101 个请求同时到来
连接池状态 — 溢出等待 第 101 个请求等待
0
空闲连接
100
活跃连接
1
等待线程
100
Pool Size
  1. 1
    前 100 个请求:与场景二相同,占满所有 100 个连接,进入 SQL 执行阶段
  2. 2
    第 101 个请求:调用 getConnection() 时,发现池中没有空闲连接,且活跃连接已达 maxPoolSize,无法创建新连接
  3. 3
    进入等待队列:第 101 个线程被阻塞,进入连接池内部的等待队列(HikariCP 用的是 SynchronousQueueLinkedBlockingQueue,取决于配置)
  4. 4
    等待期间:线程持续等待,直到:
      a) 有其他请求执行完 close() 归还了连接 → 立即获取,继续执行 正常
      b) 等待超过 connectionTimeout → 抛出 SQLException: Connection is not available 超时
  5. 5
    成功获取后:第 101 个请求拿到归还的连接,正常执行 SQL,然后归还
超时异常:如果第 101 个请求等待太久(超过 connectionTimeout,HikariCP 默认 30 秒),就会抛出异常:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms

这个异常不是 MySQL 返回的,而是连接池客户端自己抛出的——它在本地等了 30 秒还没拿到连接,就放弃了。
第 101 个请求的两种结局
结局 A:成功获取
线程阻塞等待...
某请求 close() 归还连接
池通知等待线程
第 101 个请求获取连接,继续执行
结局 B:超时失败
线程阻塞等待...
等待 5s... 10s... 25s...
超过 connectionTimeout(默认 30s)
抛出 SQLTransientConnectionException

三种场景核心对比

维度 1 个请求 100 个请求 101 个请求
空闲连接 99(余量充足) 0(刚好用完) 0(不够分配)
活跃连接 1 100 100
等待线程 0 0 1(第 101 个)
getConnection() 耗时 ~0.1ms 即时返回 ~0.1ms 即时返回 阻塞等待直到有连接归还或超时
是否创建新连接 否,复用池中连接 否,复用池中连接 否,已达 maxPoolSize 上限
请求是否阻塞 第 101 个线程阻塞
是否抛异常 可能(超时则抛异常)
MySQL 端压力 极小 较大(100 并发查询) 较大 + 客户端有积压
风险点 几乎无 慢查询可能拖慢整体 连接等待 + 慢查询 = 雪崩风险

连接释放的底层机制

理解"释放"到底发生了什么,是掌握连接池的关键。下面拆解 connection.close() 被调用后的完整过程:

// 业务代码中的标准写法 try (Connection conn = dataSource.getConnection()) { // ↑ 从池中借出连接 PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setInt(1, 42); ResultSet rs = ps.executeQuery(); // ... 处理结果 ... } // ← 这里自动调用 close() // close() 被代理拦截,实际执行: // 1. 回滚未提交的事务 // 2. 重置 autoCommit = true // 3. 清空 session 级变量 // 4. 将连接放回池的空闲队列 // 5. 通知等待中的线程(如果有)

释放 = 归还,不是断开

操作 无连接池 有连接池
close() 语义 关闭 TCP 连接 归还到池中(TCP 不断)
连接状态 连接销毁 连接重置 + 复用
下次 getConnection() 重新 TCP 三次握手 + MySQL 认证 直接从池中取出(~0.1ms)
性能差异 每次约 50~200ms 每次约 0.1ms(快 500~2000 倍)

归还后的"唤醒"机制

当第 101 个请求正在等待时,前 100 个请求中任何一个归还连接,都会触发唤醒。不同连接池的实现方式不同:

连接池 等待机制 唤醒策略
HikariCP ConcurrentBag 的 steal 机制 等待线程轮询本地队列 + 全局窃取
Druid Condition.await() 归还时 signal() 唤醒一个等待线程
c3p0 内部阻塞队列 归还时入队,等待线程自动获取

关键配置参数速查

理解了三种场景后,这些参数的意义就很清晰了:

参数 HikariCP Druid 含义
最大连接数 maximumPoolSize maxActive 池中最多多少个连接(硬上限)
获取超时 connectionTimeout maxWait 等不到连接时多久抛异常
最小空闲 minimumIdle minIdle 池中至少保留多少空闲连接
空闲超时 idleTimeout minEvictableIdleTimeMillis 连接空闲多久后可被回收
最大存活 maxLifetime maxEvictableIdleTimeMillis 连接最长活多久(防 MySQL 8h 断连)
连接泄漏检测 leakDetectionThreshold removeAbandoned 连接被借出多久未还则判定泄漏
MySQL 的 wait_timeout:MySQL 默认 wait_timeout=28800(8 小时),如果一个连接空闲超过 8 小时,MySQL 会主动断开。连接池的 maxLifetime(建议设为 25~30 分钟)确保在 MySQL 断开之前主动回收重建,避免"连接已被 MySQL 关掉但池还不知道"的问题。

交互式模拟器

拖动滑块模拟不同请求数量下的连接池状态:

请求数: 1
99
空闲连接
1
活跃连接
0
等待请求
0
超时风险
[系统] 连接池已初始化,100 个空闲连接就绪

终极风险:连接池雪崩

当请求远超连接池容量时(比如 200 个请求同时到来),可能出现级联故障:

雪崩过程 级联故障
200 请求涌入 — 100 个获取连接执行,100 个等待
SQL 执行变慢 — MySQL 并发太高,锁竞争加剧,原本 10ms 的查询变成 500ms
归还变慢 — 连接持有时间拉长,等待队列积压更严重
超时开始 — 等 30 秒后,100 个等待请求开始批量抛异常
上游超时 — 应用层报错,HTTP 请求超时,用户重试,更多请求涌入
系统崩溃 — 恶性循环,服务不可用

防御手段

手段 原理 效果
合理设置 pool size 根据 connections = ((core_count) * 2) + effective_spindle_count 公式 避免连接过多反而降低 MySQL 吞吐
缩短 connectionTimeout 从默认 30s 降到 3~5s 快速失败,避免线程长时间阻塞
SQL 超时控制 设置 statement.setQueryTimeout() 防止单条慢查询长期占用连接
连接泄漏检测 开启 leakDetectionThreshold 及时发现未 close 的连接
限流 / 熔断 Sentinel / Hystrix / Resilience4j 在请求入口拦截,保护连接池不被打满
异步 + 队列 用消息队列削峰 将同步请求转为异步处理

一张图总结

Connection Pool (maxPoolSize = 100) 空闲连接区 空闲数由请求数决定 活跃连接区 被请求占用的连接 请求 close() 归还 等待队列(仅当请求 > maxPoolSize 时出现) 线程阻塞等待,直到有连接归还或超过 connectionTimeout 超时 → SQLException: Connection is not available, request timed out after 30000ms