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