为什么你的数据库连接总超时?99% 的 Java 程序员都踩过这 5 个坑

18 阅读4分钟

为什么你的数据库连接总超时?99% 的 Java 程序员都踩过这 5 个坑

做后端开发这么多年,我见过最让程序员崩溃的场景,不是代码逻辑写错,而是数据库连接池被打爆——系统直接宕机,老板在群里疯狂 at 你。

今天这篇文章,是我去年处理了 20+ 次连接池故障后的血泪总结。我把最常见的 5 个坑整理出来,看看你踩过几个?

坑 1:连接池大小"随手配"

很多人配连接池就是抄网上的:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20

20?50?100?这个数字是怎么来的?大部分人是"感觉差不多就行"。

真实场景是这样的:

假设你的数据库最大连接数是 200,你的服务部署了 5 个实例,每个实例配了 maximum-pool-size=50。那么理论最大连接数是 250,已经超过数据库上限了。

正确的做法是:

spring:
  datasource:
    hikari:
      # 经验公式:(核心线程数 * 2) + 磁盘 IO 线程数
      # 一般建议:CPU 核心数 * 2 + 磁盘数
      maximum-pool-size: 20
      # 最小空闲连接数
      minimum-idle: 5

坑 2:连接泄漏了,还不知道

什么叫连接泄漏?就是从连接池拿了一个连接,用完了没还回去。

一个连接泄漏了不可怕,可怕的是你没发现它泄漏了,然后一个接一个,直到池子被掏空。

怎么快速定位泄漏?

HikariCP 自带了泄漏检测:

spring:
  datasource:
    hikari:
      leak-detection-threshold: 60000

设置为 60000 毫秒后,如果一个连接被借出去超过 60 秒没还,HikariCP 会打一条日志:

Long connection leak detected, elapsed time=62134ms, thread=pool-1-thread-3, ...

别小看这条日志,它就是你排查泄漏的起点。

坑 3:把连接池当队列用

我见过最离谱的代码是这样的:

List<Connection> connections = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Connection conn = dataSource.getConnection();
    connections.add(conn);
}

一次从池子里拿 10 个连接,业务处理完再逐个还回去——相当于你一个人占着 10 辆共享单车,其他人都没车骑。

正确姿势:

for (Order order : orders) {
    try (Connection conn = dataSource.getConnection()) {
        processOrder(conn, order);
    }
}

坑 4:慢 SQL 把连接卡死

假设你的连接池大小是 20,如果有一条 SQL 执行了 5 分钟,它就会占用一个连接 5 分钟。在这 5 分钟内,如果有 20 个请求进来,池子就满了。

更可怕的是,如果数据库连接超时设置是 30 秒,而你的慢 SQL 执行了 5 分钟,那么这条 SQL 可能引发重试,最终让同一个慢 SQL 同时占用多个连接。

解决方案:

spring:
  datasource:
    hikari:
      connection-timeout: 30000
      validation-timeout: 5000
      max-lifetime: 1800000

同时,务必加上慢 SQL 监控:

@Aspect
@Component
public class SlowSqlInterceptor {

    @Around("execution(* javax.sql.DataSource.getConnection())")
    public Object monitorConnection(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (cost > 1000) {
                log.warn("获取连接慢!耗时={}ms, stack={}", cost,
                         Arrays.toString(new Throwable().getStackTrace()));
            }
        }
    }
}

坑 5:忽略了数据库端的连接限制

很多人只盯着应用层配置,忘了数据库本身也有连接数限制。

MySQL 默认 max_connections 是 151,PostgreSQL 默认是 100。如果你的应用连接池配得太大,数据库会直接拒绝连接。

上线前必查:

-- MySQL 查看当前最大连接数
SHOW VARIABLES LIKE 'max_connections';

-- PostgreSQL 查看
SHOW max_connections;

-- 查看当前实际连接数
SHOW STATUS LIKE 'Threads_connected';

如果数据库连接数经常逼近上限,要么扩容数据库,要么拆分业务——别无他法。

总结

今天总结了连接池的 5 个经典坑:

症状解法
池大小乱配连接不够用或超限按容量统一规划实例总连接数
连接泄漏池子逐渐变小开启 leak-detection-threshold
攒着连接不用池子瞬间耗尽用完立即归还
慢 SQL 卡池请求堆积超时加超时控制和监控
忽略数据库限制数据库直接拒绝上线前检查 max_connections

数据库连接池的问题,说到底就是两个:拿得到用得快。把这 5 个坑填平,你的系统至少能稳一半。