连接池
上一章 JDBC 每次操作都 DriverManager.getConnection——这背后是建立 TCP 连接、数据库认证、初始化会话,耗时几十到几百毫秒。一个 Web 应用每秒几百个请求,每次都新建连接——数据库早被握手过程压垮了。
解决之道就是连接池(Connection Pool):预先建好一批连接放着,谁要用就从池里借,用完还回来——像共享单车一样循环使用。这一章我们看连接池为什么必要、怎么用、以及主流的实现 HikariCP 和 Druid。
一、连接池的必要性
先量化一下”每次新建连接”的代价:
- TCP 三次握手 —— 客户端与服务端的网络往返。
- 数据库认证 —— 用户名密码校验、权限加载。
- 会话初始化 —— 设置字符集、时区、事务隔离级别等。
- 关闭连接 —— TCP 四次挥手。
这一套下来,本地 MySQL 约 520ms,远程数据库可能 50200ms。对一次 50ms 的 SQL 查询,光是建连接就占了 80% 时间——典型的”启动开销大于业务开销”。
连接池做的事:
- 启动时预建 N 个连接放着。
- 借用时直接给一个空闲连接——O(1) 操作,无网络握手。
- 归还时不真关,标记为空闲等下次用。
- 按需扩容/缩容——忙时扩容到最大连接数,闲时缩到最小空闲。
效果:连接建立成本摊到启动期,运行时每次借连接 < 1ms。这就是连接池的”魔法”。
二、HikariCP:最快的连接池
HikariCP(“光”连接池)是 Brett Wooldridge 在 2013 年开源的连接池,号称”最快”。Spring Boot 2.0+ 默认就是它。它的快来源于:
- 字节码级优化——用 Javassist 生成代理类,避免反射。
- ConcurrentBag——自定义的并发集合,比
BlockingQueue更快。 - 无锁设计——尽量用 CAS 而非锁。
- 精简代码——源码不到 3000 行,比 DBCP 小一个数量级。
2.1 基本配置
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("123456");
config.setMaximumPoolSize(10); // 最大连接数
config.setMinimumIdle(2); // 最小空闲连接
config.setConnectionTimeout(30_000); // 获取连接超时 (ms)
config.setIdleTimeout(600_000); // 空闲连接超时 (ms)
config.setMaxLifetime(1_800_000); // 连接最大存活时间 (ms)
config.setPoolName("MyHikariPool");
DataSource ds = new HikariDataSource(config);
// 借连接 -> 用 -> 还 (close 实际是归还)
try (Connection conn = ds.getConnection()) {
// 业务 SQL...
}
注意一个关键点——从连接池借来的 Connection,调用 close() 不是真关,而是归还到池里。这是连接池的”障眼法”:业务代码和原生 JDBC 一模一样,但 close 的语义被悄悄替换了。
2.2 HikariCP 关键参数
| 参数 | 含义 | 推荐值 |
|---|---|---|
maximumPoolSize | 最大连接数 | CPU 核数 × 2 + 磁盘数(经验值) |
minimumIdle | 最小空闲 | 与 max 相同(HikariCP 推荐) |
connectionTimeout | 借连接超时 | 30s |
idleTimeout | 空闲连接存活时间 | 10min |
maxLifetime | 连接最大寿命 | 30min(要小于数据库的 wait_timeout) |
leakDetectionThreshold | 泄漏检测阈值 | 60s(debug 用) |
maximumPoolSize 怎么算? 这是经典问题。HikariCP 官方给的经验公式(来自 PostgreSQL 团队):
pool_size = (核心数 × 2) + 有效磁盘数
为什么不是越大越好?因为连接多了反而互相争抢数据库 CPU——上下文切换开销超过并行收益。一般 10~20 个连接就能撑住大部分应用。
2.3 HikariCP 原理
HikariCP 的核心数据结构是 ConcurrentBag——一个支持”窃取”的并发集合:
- 借连接时先看当前线程的 ThreadLocal 是否有”专属”连接(最近用过的),有就直接拿。
- 没有就从共享队列里 CAS 抢一个。
- 都没有就排队等待,或新建(未达 max)。
- 归还时标记为 STATE_NOT_IN_USE,唤醒一个等待线程。
这套设计避免了 BlockingQueue.poll 的锁竞争——线程优先拿自己的”专属连接”,减少跨线程传递。这就是它快的原因。
三、Druid:阿里的连接池
Druid 是阿里巴巴开源的连接池,国内用得很多。特点:
- 内置监控——自带 Web 控制台,看 SQL 执行统计、慢查询、连接池状态。
- SQL 防火墙——能拦截
DELETE FROM xxx(无 where)等危险 SQL。 - 配置丰富——比 HikariCP 多很多调优参数。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
DruidDataSource ds = new DruidDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/test");
ds.setUsername("root");
ds.setPassword("123456");
ds.setInitialSize(2); // 初始连接数
ds.setMinIdle(2); // 最小空闲
ds.setMaxActive(20); // 最大活跃
ds.setMaxWait(10_000); // 获取超时
ds.setTimeBetweenEvictionRunsMillis(60_000); // 检测间隔
ds.setMinEvictableIdleTimeMillis(300_000); // 最小空闲时间
ds.setValidationQuery("SELECT 1"); // 健康检查 SQL
ds.setTestWhileIdle(true); // 空闲时检测
// 启动监控 (内置 Servlet, Web 应用里配)
// StatViewServlet 访问 /druid/* 看监控页
Druid 的监控是杀手锏——访问 /druid/datasource.html 能看到:
- 连接池当前/峰值/活跃数
- SQL 执行次数、平均耗时、最慢 SQL
- 慢查询日志
- 错误 SQL 统计
生产环境排查 SQL 性能问题,Druid 监控比 APM 工具还直观。
四、HikariCP vs Druid
| 对比项 | HikariCP | Druid |
|---|---|---|
| 性能 | 最快 | 优秀(略逊 Hikari) |
| 监控 | 弱(依赖 Dropwizard Metrics) | 强(内置控制台) |
| SQL 防火墙 | 无 | 有 |
| 配置复杂度 | 简单 | 较多 |
| Spring Boot 默认 | 是 | 否(需引入 starter) |
| 国内使用率 | 高 | 更高 |
选型建议:
- 追求极致性能、配置简单 → HikariCP(Spring Boot 默认)。
- 需要监控、SQL 防火墙 → Druid(国内大厂常用)。
- 实际差距都不大,哪个熟用哪个。
五、连接池参数详解
理解几个关键参数对调优很重要:
5.1 最大连接数 maximumPoolSize/maxActive
池里最多能放多少连接。设小了——并发请求要排队,超时就报错;设大了——数据库压力激增,反而更慢。一般 1030,按数据库实例规格调整(MySQL 单实例通常 100500 连接上限,多个应用共享)。
5.2 最小空闲 minimumIdle/minIdle
池里至少保留多少空闲连接。低于这个值就主动建。HikariCP 官方推荐 minimumIdle == maximumPoolSize——固定大小池子,避免动态扩容的延迟。
5.3 获取超时 connectionTimeout/maxWait
借连接时等多久。超时抛 SQLTransientConnectionException。这个异常 = “池子太小或 SQL 太慢”。
5.4 空闲超时 idleTimeout
空闲连接存活多久。超了就关。要小于数据库的 wait_timeout(MySQL 默认 8 小时)——否则数据库先关了,池里还以为是好连接,下次用就报 “Communications link failure”。
5.5 最大寿命 maxLifetime
单个连接最多活多久。到了就主动重建。为什么需要?因为长时间运行的连接可能因为网络抖动、数据库重启等变”坏”——定期重建保持新鲜。一定要小于数据库的 wait_timeout,这是配置铁律。
5.6 健康检查 validationQuery/testWhileIdle
空闲时定期执行 SELECT 1 检查连接是否健康——不健康就踢出池子。HikariCP 默认用 Connection.isValid(),不需要配 validationQuery;Druid 要配。
六、实战:模拟连接池原理
下面用纯 Java 模拟一个连接池——核心就是”预建连接 + 借用 + 归还”。
观察重点:
- 预建的 2 个连接被反复借用——
c1归还后再借,复用了同一连接(id 相同)。- 超过初始大小触发扩容——第 3、4 个连接是新建的,直到达到
maxSize。- 超过 maxSize 后线程要等待——并发借第 5、6 个时,必须等前面的归还。
close()是归还不是真关——这是连接池的核心机制,业务代码用try-with-resources一样安全。- 统计借用/等待次数——等待次数多说明池子太小。
七、本章小结
| 概念 | 核心要点 |
|---|---|
| 连接池必要性 | 避免每次握手开销 |
close() 语义 | 归还而非真关 |
| HikariCP | 最快,Spring Boot 默认 |
| Druid | 阿里出品,带监控和防火墙 |
maximumPoolSize | 最大连接数,公式 核心数×2+磁盘数 |
connectionTimeout | 借连接超时 |
maxLifetime | 要小于数据库 wait_timeout |
validationQuery | 健康检查 SQL(Druid 用,HikariCP 不用) |
记忆口诀:
- 池子预建连接——启动建好,运行时借还。
close是归还——业务代码和原生 JDBC 一样,但语义被换。- HikariCP 最快——Spring Boot 默认。
- Druid 监控强——国内大厂常用。
maxLifetime < wait_timeout——避免数据库先关连接。- 不要自己造池子——HikariCP/Druid 已经够好。
结语:从连接池到网络
这一章我们看了连接池的必要性和主流实现。真实项目里你基本不会自己写连接池——直接配 HikariCP 或 Druid 即可。但理解原理能帮你调参、排查”获取连接超时”这类问题。
下一章我们离开数据库,转向网络编程——Socket 与 ServerSocket,让你能写自己的 TCP/UDP 服务器。