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

Spring Batch任务耗时超1小时触发PostgreSQL连接超时异常求助

Spring Batch 批处理任务 PostgreSQL 超时问题排查解决方案

我们有一个Spring Batch批处理任务,与PostgreSQL数据库交互时出现超时问题:任务耗时20或40分钟时运行正常,但耗时超过1小时时,服务抛出异常。

代码片段

FileListener 类

public class FileListener {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private JobLauncher jobLauncher;

    public void onEvent(Event event) {
        BatchJobEvent batchJobEvent = buildBatchJobEvent(event);
        BatchUtil.launchJob(batchJobEvent, applicationContext, jobLauncher);

    }
}

BatchUtil 类

public class BatchUtil {

    public static void launchJob(BatchJobEvent batchJobEvent, ApplicationContext applicationContext,
            JobLauncher jobLauncher) {
        Job job = (Job) applicationContext.getBean(Utils.getValue(batchJobEvent.getJobName()));
        JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();

        JobParameters jobParameters = jobParametersBuilder.toJobParameters();
        try {
            log.info("foobar: Trying to launch Job [{}], with parameters [{}]", job.getName(), jobParameters.toString());
            jobLauncher.run(job, jobParameters);
        }
    }
}

异常栈信息

{"@timestamp":"2023-03-06T16:59:01.366+00:00","@version":1,"message":"Error while extracting database name - falling back to empty error codes","logger_name":"org.springframework.jdbc.support.SQLErrorCodesFactory","thread_name":"pool-10-thread-3","level":"WARN","level_value":30000,"stack_trace":"org.springframework.jdbc.support.MetaDataAccessException: Error while extracting DatabaseMetaData; nested exception is java.sql.SQLException: Connection is closed\n\tat org.springframework.jdbc.support.JdbcUtils.extractDatabaseMetaData(JdbcUtils.java:330)\n\tat org.springframework.jdbc.support.JdbcUtils.extractDatabaseMetaData(JdbcUtils.java:355)\n\tat org.springframework.batch.core.repository.dao.JdbcExecutionContextDao.persistSerializedContext(JdbcExecutionContextDao.java:233)\n\tat org.springframework.batch.core.repository.dao.JdbcExecutionContextDao.updateExecutionContext(JdbcExecutionContextDao.java:161)\n\tat org.springframework.batch.core.repository.support.SimpleJobRepository.updateExecutionContext(SimpleJobRepository.java:209)\n\tat org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:451)\n\tat org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:330)\n\tat org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)\n\tat org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:272)\n\tat org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:81)\n\tat org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375)\n\tat org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215)\n\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\n\tat java.util.concurrent.FutureTask.run(FutureTask.java:266)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat java.lang.Thread.run(Thread.java:748)\nCaused by: java.sql.SQLException: Connection is closed\n\tat com.zaxxer.hikari.pool.ProxyConnection$ClosedConnection.lambda$getClosedConnection$0(ProxyConnection.java:490)\n\tat com.sun.proxy.$Proxy163.getMetaData(Unknown Source)\n\tat com.zaxxer.hikari.pool.ProxyConnection.getMetaData(ProxyConnection.java:361)\n\tat com.zaxxer.hikari.pool.HikariProxyConnection.getMetaData(HikariProxyConnection.java)\n\tat org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145)\n\tat org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:257)\n\tat org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:200)\n\tat org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:148)\n\tat org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:394)\n\tat org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:135)\n\tat org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:308)\n\tat org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:141)\n\tat org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50)\n\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\n\tat java.util.concurrent.FutureTask.run(FutureTask.java:266)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat java.lang.Thread.run(Thread.java:748)\nCaused by: org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.\n\tat org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:333)\n\tat org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)\n\tat org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:155)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:132)\n\tat com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)\n\tat com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)\n\tat org.springframework.jdbc.core.JdbcTemplate.lambda$update$0(JdbcTemplate.java:855)\n\tat org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:605)\n\t... 69 common frames omitted\nCaused by: java.io.EOFException: null\n\tat org.postgresql.core.PGStream.receiveChar(PGStream.java:295)\n\tat org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1947)\n\tat org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:306)\n\t... 77 common frames omitted"}

Hikari 连接池配置

hikari:
      connection-init-sql: SELECT 1
      connection-test-query: SELECT 1
      connection-timeout: 30000
      idle-timeout: 30000
      maximum-pool-size: 20
      minimum-idle: 1
      pool-name: hikari
      validation-timeout: 300000

排查与解决方案

1. 核心问题定位

从异常栈可判断,本质是数据库连接被提前关闭:任务运行超过1小时后,PostgreSQL端或中间件(防火墙/负载均衡)主动断开了连接,但Hikari连接池未检测到连接失效,仍将其分配给Spring Batch任务,导致更新执行上下文时抛出连接关闭异常。

2. 针对性配置调整

(1)适配Hikari连接池与数据库超时

确保Hikari的连接回收逻辑优先于远端断开动作,同时开启连接有效性检测:

hikari:
      connection-init-sql: SELECT 1
      # 调整空闲超时为55分钟(小于1小时,避免远端先断开)
      idle-timeout: 3300000
      maximum-pool-size: 20
      minimum-idle: 1
      pool-name: hikari
      validation-timeout: 300000
      # 开启连接泄漏检测,超时1分钟告警
      leak-detection-threshold: 60000
      # 移除connection-test-query,PostgreSQL 9.4+支持JDBC4的isValid()方法,性能更优
      validation-interval: 300000 # 每5分钟验证一次连接有效性

(2)优化Spring Batch事务配置

避免长任务持有连接过久,调整事务超时并确保Chunk级事务及时释放连接:

@Bean
public Step dataProcessStep(ItemReader<?> reader, ItemProcessor<?, ?> processor, ItemWriter<?> writer, PlatformTransactionManager transactionManager) {
    return stepBuilderFactory.get("dataProcessStep")
            .<Input, Output>chunk(1000)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .transactionManager(transactionManager)
            // 设置事务超时为2小时,覆盖默认短超时
            .transactionAttribute(new DefaultTransactionAttribute() {{
                setTimeout(7200);
            }})
            .build();
}

(3)检查并调整PostgreSQL端超时

登录PostgreSQL执行以下命令,确保数据库端超时设置适配任务时长:

-- 查看当前空闲事务超时设置
SHOW idle_in_transaction_session_timeout;
-- 查看TCP保活配置
SHOW tcp_keepalives_idle;
SHOW tcp_keepalives_interval;

-- 若需要,设置空闲事务超时为1.5小时(单位:毫秒)
SET idle_in_transaction_session_timeout = '5400000';

3. 额外优化建议

  • 拆分长任务:将大文件拆分为多个小文件,降低单任务运行时长,减少连接持有时间。
  • 启用异步JobLauncher:替换默认同步执行器,避免主线程阻塞并优化线程资源利用:
@Bean
public JobLauncher asyncJobLauncher(JobRepository jobRepository) {
    SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
    jobLauncher.setJobRepository(jobRepository);
    jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor("batch-executor-"));
    return jobLauncher;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 05:25:03