凌晨三点,你的告警开始疯狂作响。服务不可用了。你抓起电脑一顿操作:重启、应用回滚、扩容——都没用。数据库CPU正常,内存正常,慢查询日志里什么都没有。但就是连不上。
你可能遇到了连接池耗尽。
这玩意儿有多操蛋?它是那种你写代码的时候根本不会多想一行的东西。你import一个库,设个pool_size=10,然后继续写你的业务逻辑。直到有一天,你的应用开始报错:too many connections,然后你开始怀疑人生。
连接池是什么,为什么你一直在忽略它
数据库连接池本质上就是一个连接缓存池。每次你要查数据库,不用重新建立TCP连接、跑一遍认证——池子直接给你一个现成的。用完再还回去。
听起来很美好对吧?但问题来了:
池子里的连接数量是有限的。你要是借了不还,或者借的速度比还的速度快,池子就空了。空了之后怎么办?等。等别的连接还回来。如果你的查询执行时间很长,或者你根本忘了还——恭喜你,你创造了一个死锁。
而且这玩意儿特别隐蔽。开发环境你一个人测,连接池够用。上线后100个人并发,池子就开始喘了。等你规模再大点——Boom。
我见过的那些连接池惨案
惨案一:默认配置走天下
这是最常见的。HikariCP默认pool_size=10。你问10够不够用?看你做什么。如果只是偶尔查个用户信息,够了。如果你要跑一个批量导入处理10000条数据,10个连接分分钟被榨干。
更骚的是,很多人根本不知道自己用的什么连接池。Spring Boot默认HikariCP,之前的老项目可能是Druid,再老一点可能是c3p0。每一个的默认配置都不一样。你以为你在调优,其实你在盲人摸象。
惨案二:连接泄漏
什么叫连接泄漏?就是你拿了连接,用完了,但是没有还回去。
看这段代码:
public User getUserById(Long id) {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("name"));
}
return null;
// 连接呢?还了吗?
// 没有!!!
}
这段代码在正常情况下能跑,但每次调用都会泄漏一个连接。跑个1000次,池子就空了。你的下一个请求只能在那等着。
正确姿势:
public User getUserById(Long id) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ResultSet rs = ps.executeQuery()) {
ps.setLong(1, id);
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("name"));
}
return null;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
用try-with-resources,连接自动归还。这是基本功,但依然有人写十几年Java还在犯这个错。
惨案三:慢SQL堵死池子
你的池子有20个连接。有1个查询特别慢,要跑30秒。结果这30秒里,这1个连接一直被占用着。如果这种慢查询一多,你的池子实际上可用连接数就急剧下降。
更坑的是什么?你的监控可能显示数据库CPU不高、IO不高,一切正常。但应用就是不可用。因为瓶颈不在数据库,在连接池。
这种情况怎么破?几个思路:
- 查询超时:给每个查询设置
query timeout,超过时间直接抛异常而不是干等 - 慢查询日志:把超过5秒的SQL都记下来,丢给DBA优化
- 连接池熔断:HikariCP有
connectionTimeout,拿不到连接就快速失败,别在那死等
惨案四:微服务里每个服务都建池
你有10个微服务,每个都连同一个数据库。每个服务pool_size=10。10x10=100。数据库最大连接数是多少?可能是100,也可能是50。你品品。
这种问题在大规模微服务架构里特别容易出现。每个团队各写各的,没人统一规划数据库连接数。等上线一跑,数据库连接数直接爆表。
解法:数据库连接池作为共享资源。所有微服务共享一个连接池代理,或者统一管理连接数分配。听起来不优雅,但好用。
惨案五:连接被TCP层卡死
这个比较阴间。你的应用和数据库之间网络抖动了一下。TCP挥手没完成,连接处于TIME_WAIT状态。数据库那边觉得这个连接还在,但实际上应用这边已经断了。
HikariCP有keepaliveTime参数解决这个问题。它会定期检查池子里的连接还活不活着,不活的直接剔除。这个参数很多人不设,觉得没必要。等网络抖动一下你就知道为什么要设了。
怎么判断你的池子出了问题
几个关键指标:
- 活跃连接数接近池子上限:说明你的并发量快把池子榨干了
- 获取连接等待时间变长:正常应该是毫秒级,如果开始到秒级,说明在排队
- 连接使用时间变长:原来是1ms,现在变成100ms,可能是SQL变慢了,也可能是连接泄漏
HikariCP自带监控endpoint:/actuator/prometheus。把这个接进Prometheus/Grafana,你就能看到池子的实时状态了。别光看数据库QPS,连接池指标同样重要。
配置连接池的正确姿势
以HikariCP为例,说几个关键参数怎么设:
spring:
datasource:
hikari:
# 连接池最大size。这个数字不是越大越好
# 计算公式:connections = ((core_count * 2) + effective_spindle_count)
# 内存数据库没磁盘IO瓶颈,可以设大点
maximum-pool-size: 20
# 等待连接超时。拿不到连接就报错,别傻等
connection-timeout: 30000
# 连接最大生命周期。数据库那边有连接超时限制的话,这个要设小一点
max-lifetime: 1800000
# 保持连接活跃检测。防止连接被网络设备干掉
keepalive-time: 30000
maximum-pool-size怎么设?有个公式:
连接数 = (核心数 * 2) + 有效磁盘数
这个公式适用于传统数据库。如果你用的是SSD,没有磁盘IO瓶颈,可以适当加大。如果是内存数据库如Redis,连接数可以设得更大。
但现实是,很多人设连接池大小就是瞎猜。10不够设20,20不够设50。应该反过来:根据数据库最大连接数和你的服务实例数,反推每个实例应该设多少。
说点扎心的
连接池这个问题,说难听点,是那种谁都能写代码,谁都不管的角落。业务代码有人review,架构设计有人评审,但连接池参数?没人看,反正能跑就行。
结果就是,系统小的时候没事,系统大了就开始周期性抽风。查了一圈监控,数据库没事,应用没事,最后发现是连接池配置太保守或者太激进。
我见过最离谱的一个案例:生产库最大连接数是200,应用有4个实例,每个实例pool_size设的50。4x50=200。看起来刚好对吧?但他们还有个后台管理系统也连同一个库,平时不挂,几个人用的时候没事,大促时一堆运营同时上去查数据——连接直接爆了。
这种问题,代码review看不出来,自动化测试测不出来。只能靠对系统整体有了解的人,在设计阶段就想清楚。
给你的建议
- 不要用默认配置:生产环境的连接池大小一定要根据实际并发量和数据库能力来算
- 连接必须还回去:try-with-resources学一下,花不了多少时间
- 监控要接上:连接池的指标和数据库QPS一样重要
- 设置合理的超时:连接获取超时、查询超时、连接生命周期,都要设
- 容量规划要算上连接池:微服务架构里尤其容易忽略
连接池不是一个「设完就不用管」的东西。它是应用和数据库之间的城门。城门破了,里面再坚固也没用。
下次写代码的时候,花30秒想想你的连接池。这30秒可能省掉你一次凌晨三点的排障。