You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java虚拟线程场景下JDBC连接池耗尽问题排查与解决咨询

虚拟线程导致数据库连接耗尽问题的解决思路

问题背景

我们有一个基于Java 17的服务,执行本地逻辑后向第三方系统发送请求,第三方响应时间约800-1400ms。此前使用大小为12的ThreadPoolExecutor(基础设施无法扩容),请求发送速率为5-8请求/秒。升级到Java 21并使用虚拟线程后,请求速率提升至约50请求/秒,但出现JDBC连接耗尽的异常。

数据库连接池上限约100:服务启动时占用10个连接;使用ThreadPoolExecutor时新增13个,总计23个;使用newVirtualThreadPerTaskExecutor时连接数飙升至94个,触发以下错误:

FATAL: remaining connection slots are reserved for non-replication superuser connections

我们用以下SQL监控PostgreSQL 13.9的连接情况:

SELECT pid, datname, usename, application_name, client_addr, client_port, backend_start, query_start, state_change, query, state
FROM pg_stat_activity where application_name ='PostgreSQL JDBC Driver';

典型异常栈示例:

org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:466) ~[spring-orm-6.1.2.jar:6.1.2]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:531) ~[spring-tx-6.1.2.jar:6.1.2]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:405) ~[spring-tx-6.1.2.jar:6.1.2]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:610) ~[spring-tx-6.1.2.jar:6.1.2]
    ...
    at java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314) ~[na:na]
    at java.base/java.lang.VirtualThread.run(VirtualThread.java:309) ~[na:na]
Caused by: org.hibernate.exception.GenericJDBCException: Unable to acquire JDBC Connection [FATAL: remaining connection slots are reserved for non-replication superuser connections] [n/a]

我们的观察:

  • ThreadPoolExecutor的12个固定线程会复用连接,连接数稳定;
  • 虚拟线程在等待第三方响应时会释放载体线程,导致大量新虚拟线程被创建,每个线程都申请新连接,最终耗尽连接池。

尝试过的无效方案:

  • 设置VM参数-Djdk.virtualThreadScheduler.maxPoolSize=5甚至1,无法限制虚拟线程数量;
  • 用计数器/信号量限制虚拟线程,虽降低吞吐量,但连接数仍远高于ThreadPoolExecutor时期。

疑问:为什么虚拟线程会频繁创建新连接,而ThreadPoolExecutor能复用线程绑定的连接?如何限制并发任务数和数据库连接数,实现连接复用?

相关代码:
虚拟线程初始化:

this.executorService = Executors.newSingleThreadScheduledExecutor();
this.taskExecutorService = Executors.newVirtualThreadPerTaskExecutor();

this.executorService.scheduleAtFixedRate(this, 0L, this.pollingTime.toMillis(), TimeUnit.MILLISECONDS); //服务启动时调用一次

public void run() {
    this.taskExecutorService.execute(//我的可运行任务);
}

JDBC连接代码:

public Optional<Job> getNextJob(String queue) {
    Optional<Job> job = Optional.empty();

    try {
        Connection connection = this.dataSource.getConnection();

        try {
            String sql = "DELETE FROM scheduler_task WHERE id = (SELECT id FROM scheduler_task WHERE queue = ? and trigger_date < now() LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING id, queue, reference_id, trigger_date";
            PreparedStatement stmt = connection.prepareStatement(sql);

            try {
                ResultSet resultSet = stmt.executeQuery();

                try {
                    if (resultSet.next()) {
                        job = Optional.of(this.mapToJob(resultSet));
                    }
                } catch (Throwable var14) {
                    if (resultSet != null) {
                        try {
                            resultSet.close();
                        } catch (Throwable var13) {
                            var14.addSuppressed(var13);
                        }
                    }
                    throw var14;
                }

                if (resultSet != null) {
                    resultSet.close();
                }
            } catch (Throwable var15) {
                if (stmt != null) {
                    try {
                        stmt.close();
                    } catch (Throwable var12) {
                        var15.addSuppressed(var12);
                    }
                }

                throw var15;
            }
            if (stmt != null) {
                stmt.close();
            }
        } catch (Throwable var16) {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Throwable var11) {
                    var16.addSuppressed(var11);
                }
            }

            throw var16;
        }
        if (connection != null) {
            connection.close();
        }

        return job;
    } catch (SQLException var17) {
        log.error("error", var17);
        throw new Exception(var17);
    }
}

原因分析

  1. 虚拟线程的无限制并发特性:newVirtualThreadPerTaskExecutor会为每个任务创建新虚拟线程,而虚拟线程在IO阻塞(比如等待第三方响应)时会释放载体线程,让载体线程处理其他虚拟线程。这导致短时间内可以同时运行大量虚拟线程,每个线程都去申请数据库连接,远超原线程池的并发量。
  2. 连接复用逻辑的差异:原ThreadPoolExecutor的固定线程可以通过ThreadLocal绑定连接,实现线程级的连接复用;而虚拟线程是一次性的,每个虚拟线程都会从连接池申请新连接,用完后归还,但并发量过大时连接池被快速占满。
  3. 对jdk.virtualThreadScheduler.maxPoolSize的误解:这个参数限制的是载体线程(平台线程)的数量,不是虚拟线程的数量。即使设置为5,仍然可以创建大量虚拟线程,只是它们会在5个载体线程上调度,无法限制虚拟线程的并发数。

解决方案

1. 限制虚拟线程的并发执行数

不要直接使用无限制的newVirtualThreadPerTaskExecutor,而是创建带并发上限的虚拟线程池,控制同时运行的虚拟线程数量:

// 根据数据库可用连接数设置并发上限,比如20
int maxConcurrency = 20;
ExecutorService taskExecutorService = new ThreadPoolExecutor(
    maxConcurrency,
    maxConcurrency,
    0L,
    TimeUnit.MILLISECONDS,
    new SynchronousQueue<>(),
    Thread.ofVirtual().factory()
);

这样可以避免过多虚拟线程同时申请连接,将连接数控制在合理范围内。

2. 优化数据库连接池配置

  • 设置合理的最大连接数:根据数据库分配给该服务的连接配额(比如30个),调整连接池的maximumPoolSize(以HikariCP为例)为30,确保不会超过数据库的连接上限。
  • 配置连接回收策略:设置idleTimeout(比如30秒)和maxLifetime(比如1小时),及时回收空闲连接,避免连接泄漏。
  • 开启连接有效性验证:配置connectionTestQuery为SELECT 1,确保获取的连接是可用的,避免无效连接占用资源。

3. 简化JDBC连接的使用逻辑

确保连接被正确关闭,避免泄漏。使用try-with-resources语法自动管理资源,简化代码:

public Optional<Job> getNextJob(String queue) {
    String sql = "DELETE FROM scheduler_task WHERE id = (SELECT id FROM scheduler_task WHERE queue = ? and trigger_date < now() LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING id, queue, reference_id, trigger_date";
    try (Connection connection = this.dataSource.getConnection();
         PreparedStatement stmt = connection.prepareStatement(sql)) {
        stmt.setString(1, queue);
        try (ResultSet resultSet = stmt.executeQuery()) {
            if (resultSet.next()) {
                return Optional.of(this.mapToJob(resultSet));
            }
        }
    } catch (SQLException e) {
        log.error("获取任务失败", e);
        throw new RuntimeException(e);
    }
    return Optional.empty();
}

try-with-resources会自动关闭Connection、PreparedStatement和ResultSet,避免手动关闭时出现的泄漏问题。

4. 调整任务调度逻辑

  • 避免任务堆积:将scheduleAtFixedRate改为scheduleWithFixedDelay,确保上一个任务执行完成后再调度下一个任务,避免短时间内提交大量任务。
  • 批量获取任务:修改getNextJob为批量获取多个任务(比如一次获取10个),减少数据库连接的申请次数,提升处理效率。

5. 控制事务并发数(Spring环境)

如果使用Spring事务管理,确保同时执行的事务数不超过连接池容量。可以通过配置Spring的TaskExecutor带并发限制,或者在事务方法上添加限流逻辑,避免过多事务同时占用连接。


内容的提问来源于stack exchange,提问作者Shoaib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 23:59:50