连接池:那个你天天用,却从来没配对的东西
干后端这些年,我发现一个特别有意思的现象:大部分程序员都会配置连接池,但几乎没人能说清楚自己配的值是怎么来的。你问他:「你这个数据库连接池大小为什么是20?」他眨眨眼:「呃……这是我抄的。」
连接池配置错了,轻则响应慢如牛,重则服务雪崩。今天我就把几个经典的「连接池惨案」场景拆开揉碎讲给你听,看完你可能会后背发凉——因为说不定你线上现在就中招了。
场景一:线程池和连接池的「双人舞」
先问个问题:你的服务有50个线程处理请求,数据库连接池大小是20,请求来了会怎样?
答案是:线程会排队等连接。因为同时只有20个线程能拿到数据库连接,剩下的30个在那干等着。如果等待时间过长,FGC一来,等超时的请求会把整条链路堵死。
这个场景太经典了,经典到有个公式:
connections = (core_count * 2) + effective_spindle_count
这是Oracle官方给出的建议公式,听起来很美好对吧?但我要告诉你一个暴论:这个公式在现代服务架构下,基本等于废话。
为什么?因为这个公式是给物理机时代设计的。想想看:你的服务跑在Kubernetes里,Pod本身可以被调度到任意节点,节点上到底有多少个核心?鬼知道。你的应用拿到的只是容器视角下的「虚拟核心数」,和物理机的NUMA拓扑完全不是一回事。
实际情况是:连接池大小的最优值,本质上取决于你的查询到底在「等」什么。
- 如果瓶颈在CPU计算(复杂SQL、全表扫描)→ 连接数可以适当多一些
- 如果瓶颈在IO等待(网络延迟、磁盘IO)→ 连接数要保守,否则白白耗尽
- 如果瓶颈在数据库本身(连接数上限已占满)→ 加连接池没用,得优化查询
场景二:那个连接泄漏的「幽灵」
连接泄漏是什么?就是申请了连接,用完了没还回去。更可怕的是,这种泄漏通常是「慢性」的——一开始完全正常,跑着跑着连接就开始不够用了,直到某天服务突然雪崩,你翻开监控一看:「嗯?连接数怎么500了?我明明配的max200啊?」
看个真实案例代码:
public User getUserById(Long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
return mapToUser(rs);
}
return null;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 经典错误:只关闭了rs,没关闭stmt和conn
if (rs != null) rs.close();
// stmt漏了,conn也漏了
}
}
这段代码我见过至少十几次。开发者以为自己在finally里做清理了,但实际上只关了ResultSet,Statement和Connection全漏了。在低流量的时候问题不大,但在高并发场景下,每秒漏几个连接,很快就把池子榨干。
正确的写法是用try-with-resources,让JVM自动帮你关:
public User getUserById(Long id) {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
stmt.setLong(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapToUser(rs);
}
}
return null;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
有人会说:等等,我看了一些老代码里是用了连接池的,为什么还是泄漏了?答案是:有些框架会自动管理连接生命周期,但你的手写代码不一定会。最好在你用的ORM层(MyBatis/Hibernate/JPA)上统一埋点监控——getConnection().getConnectionPoolStats()这类API该用就用,别等告警了才知道漏了。
场景三:HTTP连接池——那个被忽视的「小而美」
说完数据库连接池,再说说HTTP客户端连接池。这个东西坑的人比数据库连接池还多,因为很多开发者根本不认为这是个池子。
典型场景:前端调后端,后端调三方API。很多人写HTTP调用代码是这样的:
HttpClient client = HttpClient.newHttpClient();
// 每次请求都new一个request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.build();
// 每次都用同一个client,默认连接池大小是多少?无限?
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
看起来没问题对吧?但是!如果你用的是HttpClient的默认实例,而且每次都new HttpClient()——恭喜你,每个HTTP请求都可能建立一条新的TCP连接,没有复用。DNS变化、TIME_WAIT堆积、HTTPS握手开销……你的延迟就这么上去了。
正确的姿势是:把HttpClient做成单例或者池化复用。在Spring生态里,RestTemplate要配连接池,WebClient更是默认就有连接池管理(只要你不手动关掉)。
// 正确的HTTP连接池配置示例(Apache HttpClient)
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200); // 最大连接数
cm.setDefaultMaxPerRoute(50); // 每个路由最大连接数(比如对单个域名的)
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(3000)
.setSocketTimeout(5000)
.setConnectionRequestTimeout(2000)
.build();
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultRequestConfig(requestConfig)
.build();
还有个经典坑:连接池配置了,但是没有设置evictExpiredConnections()和closeExpiredConnections()的定时任务。池子里的连接可能早就过期了,但还在池子里躺着占位置,下次请求拿到的可能就是个坏连接。超时?理所当然。
场景四:连接池耗尽时的「惊群效应」
最后说一个最恐怖的场景,我管它叫「惊群效应」。
想象一下:你的服务正常运转,突然数据库因为一次慢查询堵住了,这时候会发生什么?请求开始堆积,连接池被占满,新来的请求进不来,开始等。等的时间越来越长,timeout触发,请求失败,用户刷新页面,又一堆新请求打过来……恶性循环,服务直接崩溃。
更可怕的是,这时候你即使扩容也没用——因为瓶颈不在你的服务,在数据库。每一个新启动的Pod,都是去抢那点可怜的数据库连接。
应对策略是什么?答案是:熔断和限流。当数据库连接池使用率超过某个阈值(比如80%),新的请求应该直接拒绝(返回降级响应),而不是在那排队等超时。把压力扛在自己身上,比把整条链路拖垮要强得多。
具体实现可以用Sentinel或者Resilience4j:
@CircuitBreaker(name = "database", fallbackMethod = "getUserFallback")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
public User getUserFallback(Long id, Throwable t) {
// 数据库熔断时的降级逻辑
log.warn("数据库熔断,返回降级数据,id={}", id);
return getCachedUser(id); // 或者返回一个默认空对象
}
熔断不是万能药,但在连接池即将耗尽的瞬间,它是唯一能救你一命的东西。
总结:连接池配置的几条军规
说了这么多,给你总结几条可以直接抄走的结论:
- 连接池大小不是越大越好——它要和你的线程数、查询特性、数据库承载能力匹配。没有标准答案,只有适合答案。
- 监控比配置更重要——连接池使用率、等待时间、泄漏检测,这些指标不监控,配再好也是盲人摸象。
- 连接用完必须还——try-with-resources是标配,别在这上面偷懒。
- HTTP连接池和数据库连接池同等重要——三方调用的连接泄漏同样致命。
- 提前想好熔断策略——等连接池耗尽再救火,黄花菜都凉了。
好了,今天就唠到这儿。我是爱吃小龙虾 🦞,下次再见!