1. 概述

默认情况下,当 Spring 应用关闭时,其内置的 TaskExecutor 会直接中断所有正在运行的任务。这种“粗暴”的方式可能导致任务执行到一半被强行终止,数据不一致或资源未释放,带来风险。

而我们更希望的是:在关闭前,让正在执行的任务有机会完成,从而实现“优雅停机”(Graceful Shutdown)。这种方式能显著提升系统的稳定性,尤其是在处理异步任务、消息消费或定时任务的场景中。

本文将聚焦于如何在 Spring Boot 中,通过合理配置线程池,实现任务的优雅停机。

2. 默认行为:任务被粗暴中断

我们先看一个简单示例。假设有一个 Spring Boot 应用,注入了默认的 TaskExecutor

@Autowired
private TaskExecutor taskExecutor;

在应用启动时,提交一个耗时 60 秒的任务:

taskExecutor.execute(() -> {
    Thread.sleep(60_000);
});

问题来了:如果在应用启动后第 20 秒手动触发关闭(比如发送 SIGTERM 或调用 /actuator/shutdown 接口),那么这个正在执行的任务会被立即中断(InterruptedException),应用随即退出。

❌ 这显然不是我们想要的——任务还没做完就被杀了,可能造成数据丢失或状态不一致。

3. 配置线程池等待任务完成

要解决这个问题,我们需要自定义 ThreadPoolTaskExecutor,并开启“等待任务完成”的开关。

关键配置如下:

@Bean
public TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(2);
    taskExecutor.setMaxPoolSize(2);
    taskExecutor.setWaitForTasksToCompleteOnShutdown(true); // ✅ 开启等待
    taskExecutor.initialize();
    return taskExecutor;
}

setWaitForTasksToCompleteOnShutdown(true) 的作用:

  • 收到关闭信号后,不再接受新任务(关闭任务队列)
  • 允许正在执行的任务继续运行
  • 允许排队中的任务被执行完毕

我们再看一个更典型的测试场景:启动时提交 3 个 60 秒的任务,但线程池最大只有 2 个线程:

@PostConstruct
public void runTaskOnStartup() {
    for (int i = 0; i < 3; i++) {
        taskExecutor.execute(() -> {
            try {
                Thread.sleep(60_000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

假设我们在第 10 秒触发关闭:

  • 前两个任务正在执行,会继续跑完
  • 第三个任务在队列中,也会被执行
  • 所以整个应用会在约 70 秒后才真正退出(10 + 60)

⚠️ 注意:虽然任务能继续执行,但 Spring 容器的其他组件可能已经开始关闭,比如数据库连接池、消息中间件客户端等,这可能导致任务中途失败。

4. 设置最大等待时间,防止无限等待

上面的配置虽然能让任务继续执行,但 Spring 容器的关闭流程并不会无限等待。为了阻塞容器的后续关闭步骤,直到任务完成或超时,我们需要设置一个最大等待时间:

taskExecutor.setAwaitTerminationSeconds(120); // 等待最多 120 秒

⚙️ 配置组合建议:

@Bean
public TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(2);
    taskExecutor.setMaxPoolSize(2);
    taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    taskExecutor.setAwaitTerminationSeconds(120); // 阻塞容器关闭最多 120 秒
    taskExecutor.initialize();
    return taskExecutor;
}

✅ 这样配置后的行为:

  • 收到 shutdown 信号
  • 线程池停止接收新任务
  • 容器关闭流程被阻塞,最多等待 120 秒
  • 在此期间,所有运行中和队列中的任务可正常完成
  • 超时后,无论任务是否完成,强制中断并继续关闭

💡 经验建议awaitTerminationSeconds 应根据业务任务的最大耗时合理设置,建议预留 buffer,比如最长任务 60 秒,可设为 90~120 秒。

5. 总结

通过自定义 ThreadPoolTaskExecutor 并启用以下两个关键配置,即可实现 Spring Boot 的优雅停机:

setWaitForTasksToCompleteOnShutdown(true)
setAwaitTerminationSeconds(N)

优点:

  • 避免任务被粗暴中断
  • 提高系统稳定性,减少数据不一致风险
  • 适合处理异步任务、消息消费、定时任务等场景

缺点:

  • ❌ 停机时间变长,可能影响发布效率
  • ❌ 若任务本身阻塞或死循环,会导致停机卡住(需业务层保障任务可终止)

最佳实践建议:

  • 对于关键任务,务必开启优雅停机
  • 设置合理的超时时间,避免无限等待
  • 结合 Spring Boot Actuator 的 /actuator/shutdown 接口或 Kubernetes 的 preStop 钩子使用,效果更佳

示例代码已托管至 GitHub:https://github.com/example/spring-boot-graceful-shutdown


原始标题:Graceful Shutdown of a Spring Boot Application