当 pool size = 100 时,1 个请求 / 100 个请求 / 101 个请求分别会发生什么?
连接池(Connection Pool)是一组预先创建好的数据库连接的缓存。应用启动时,连接池会初始化一定数量的 TCP 连接到 MySQL,后续请求直接从池中借出连接使用,用完归还,避免反复建立/销毁 TCP 连接的开销。
maxPoolSize(最大连接数)、connectionTimeout(获取连接超时时间)、idleTimeout(空闲连接存活时间)、maxLifetime(连接最大存活时间)。不同的连接池实现(HikariCP、Druid、c3p0)参数名称略有不同,但语义一致。
connection.close() 并不是关闭 TCP 连接!连接池对 Connection 对象做了代理(Proxy),close() 被拦截后实际执行的是将连接状态重置并归还到池中,TCP 连接依然存活。
dataSource.getConnection()connection.close(),代理对象拦截此调用,将连接重置状态(清空事务上下文、回滚未提交事务等),放回池的空闲队列。空闲连接数恢复为 100close() 被调用的时刻。在 JDBC 中,标准写法是 try-with-resources 或 finally 块中 close。释放不是销毁 TCP 连接,而是归还到池中。如果代码忘记 close,连接就被泄漏(leak),池中可用连接越来越少,最终耗尽。
getConnection()ConcurrentBag(HikariCP)或锁机制保证线程安全,逐一分配。最终 100 个连接全部分配完毕connectionTimeout 之前拿到连接就不会超时。此场景下每个请求都立刻拿到了连接,只是 MySQL 端需要同时处理 100 个查询,可能因锁竞争、CPU 调度等原因导致 SQL 执行变慢。
getConnection() 时,发现池中没有空闲连接,且活跃连接已达 maxPoolSize,无法创建新连接SynchronousQueue 或 LinkedBlockingQueue,取决于配置)close() 归还了连接 → 立即获取,继续执行 正常connectionTimeout → 抛出 SQLException: Connection is not available 超时
connectionTimeout,HikariCP 默认 30 秒),就会抛出异常:java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms| 维度 | 1 个请求 | 100 个请求 | 101 个请求 |
|---|---|---|---|
| 空闲连接 | 99(余量充足) | 0(刚好用完) | 0(不够分配) |
| 活跃连接 | 1 | 100 | 100 |
| 等待线程 | 0 | 0 | 1(第 101 个) |
| getConnection() 耗时 | ~0.1ms 即时返回 | ~0.1ms 即时返回 | 阻塞等待直到有连接归还或超时 |
| 是否创建新连接 | 否,复用池中连接 | 否,复用池中连接 | 否,已达 maxPoolSize 上限 |
| 请求是否阻塞 | 否 | 否 | 第 101 个线程阻塞 |
| 是否抛异常 | 否 | 否 | 可能(超时则抛异常) |
| MySQL 端压力 | 极小 | 较大(100 并发查询) | 较大 + 客户端有积压 |
| 风险点 | 几乎无 | 慢查询可能拖慢整体 | 连接等待 + 慢查询 = 雪崩风险 |
理解"释放"到底发生了什么,是掌握连接池的关键。下面拆解 connection.close() 被调用后的完整过程:
| 操作 | 无连接池 | 有连接池 |
|---|---|---|
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 |
连接被借出多久未还则判定泄漏 |
wait_timeout=28800(8 小时),如果一个连接空闲超过 8 小时,MySQL 会主动断开。连接池的 maxLifetime(建议设为 25~30 分钟)确保在 MySQL 断开之前主动回收重建,避免"连接已被 MySQL 关掉但池还不知道"的问题。
拖动滑块模拟不同请求数量下的连接池状态:
当请求远超连接池容量时(比如 200 个请求同时到来),可能出现级联故障:
| 手段 | 原理 | 效果 |
|---|---|---|
| 合理设置 pool size | 根据 connections = ((core_count) * 2) + effective_spindle_count 公式 |
避免连接过多反而降低 MySQL 吞吐 |
| 缩短 connectionTimeout | 从默认 30s 降到 3~5s | 快速失败,避免线程长时间阻塞 |
| SQL 超时控制 | 设置 statement.setQueryTimeout() |
防止单条慢查询长期占用连接 |
| 连接泄漏检测 | 开启 leakDetectionThreshold |
及时发现未 close 的连接 |
| 限流 / 熔断 | Sentinel / Hystrix / Resilience4j | 在请求入口拦截,保护连接池不被打满 |
| 异步 + 队列 | 用消息队列削峰 | 将同步请求转为异步处理 |