分类:架构与高并发 · 作者:管理员 · 发布于 2026-09-19 · 阅读 0 · 标签:Spring Boot / 线程池 / @Async / 优雅关闭 / 高并发

一、背景:你组的线程池真的配对了吗?

线程池是 Spring Boot 服务中最基础也最容易出问题的组件之一。绝大多数项目初期只靠 ThreadPoolTaskExecutor 的默认配置就上了线,等到双十一大促流量一冲——队列积压、核心线程打满、拒绝策略触发,甚至服务直接卡死。本文从一个电商中台的实际案例入手,覆盖线程池的隔离设计、参数调优、异常处理、上下文传递与优雅关闭的完整链路。

二、核心配置:ThreadPoolTaskExecutor 与 ThreadPoolExecutor

Spring Boot 推荐使用 ThreadPoolTaskExecutor 而非裸 ThreadPoolExecutor,前者由 Spring 管理生命周期并自动绑定 DelegatingSecurityContextExecutor。一个生产级的配置:

@Configuration
public class ThreadPoolConfig {

    @Bean("commonExecutor")
    public ThreadPoolTaskExecutor commonExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数:CPU 密集型 2-4,IO 密集型酌情放大
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        // 队列容量:200 意味着瞬时突发可缓冲,超出触发 rejection
        executor.setQueueCapacity(200);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("common-");
        // 拒绝策略:CallerRunsPolicy 让调用的主线程自己跑,不丢任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务完成再 shutdown
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }

    @Bean("asyncExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(15);
        executor.initialize();
        return executor;
    }
}

⚠️ 两个最容易踩的坑:

  • @Async 默认使用 SimpleAsyncTaskExecutor:它为每个请求新开线程,完全不复用,高并发下无限创建导致 OOM。必须显式声明自定义 Executor Bean 并指定 @Async("asyncExecutor")。
  • CallerRunsPolicy 不是银弹:它虽不丢任务,但让调用线程(如 Tomcat 的请求处理线程)去跑异步任务,响应时间飙升。对延迟敏感接口应改用 DiscardPolicy + 补偿机制。

三、线程隔离:不同业务用不同的池

核心原则:一个池死掉不影响另一个池。推荐按业务维度拆分:

  • 订单处理池:core=4,max=8,queue=100,独立名称
  • 推送通知池:core=2,max=4,queue=50,失败可重试
  • 数据分析池:core=2,max=4,queue=200,不要求实时

配合 @Async 注解指定 Bean 名称即可:

@Service
public class OrderService {
    @Async("orderExecutor")
    public CompletableFuture<OrderResult> processOrder(OrderDTO dto) {
        // 实际的订单处理逻辑
        return CompletableFuture.completedFuture(result);
    }
}

四、异步异常处理:别让异常静默消失

默认 @Async 方法抛出的异常不会进入 Controller 的异常处理。必须注册 AsyncUncaughtExceptionHandler:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        return asyncExecutor(); // 引用上面的 Bean
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            log.error("异步方法执行异常: {}#{},参数: {}",
                method.getDeclaringClass().getSimpleName(),
                method.getName(), params, ex);
            // 发送告警通知(钉钉/微信/邮件)
            alertService.sendAlert("异步任务异常: " + ex.getMessage());
        };
    }
}

⚠️ CompletableFuture 的异常陷阱

使用 CompletableFuture.supplyAsync() 或 runAsync() 时,异常被封装在返回的 CompletableFuture 里。调用 future.get() 或 future.join() 才会抛出 ExecutionException。如果不调用 get/join,异常完全不会触发 AsyncUncaughtExceptionHandler——这就是为什么线上日志里"任务跑完了但结果丢了"。

五、上下文传递:MDC & SecurityContext 跨线程

主线程的 MDC(日志链路追踪)和 SecurityContext 在异步线程中默认丢失。你需要装饰器:

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable task) {
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        return () -> {
            if (mdcContext != null) {
                MDC.setContextMap(mdcContext);
            }
            try {
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

在 ThreadPoolTaskExecutor 上设置:executor.setTaskDecorator(new MdcTaskDecorator());。这样异步方法也能输出完整的 traceId 日志链路,排查问题时不必跨线程瞎翻。

同理,SecurityContext 用 SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL) 或自定义 TaskDecorator 手动传递。

六、优雅关闭:拒绝"停机就丢请求"

滚动发布或下线时,如果直接 kill 进程,正在处理的异步任务全部丢失。工程化做法:

  1. 配合 Spring Boot Actuator:先通过 /actuator/shutdown 或 /actuator/pause 停止接受新请求;
  2. 等线程池 drain 完:设置 setWaitForTasksToCompleteOnShutdown(true) + setAwaitTerminationSeconds(30);
  3. K8s 或自定义脚本中给足宽限:preStop hook 等待 30-45 秒,让 Spring 完成线程池的 drain。
@PreDestroy
public void onShutdown() {
    log.info("==== 开始优雅关闭线程池 ====");
    // 显式 shutdown 所有自定义 Executor
    // Spring 管理的 Bean 会自动调用,但日志输出确认很有必要
}

血的教训:某项目只配了 5 秒 awaitTermination,大促时线程池里堆积了上千个任务,5 秒根本跑不完,直接丢弃。实测经验:给线程池 30 秒缓冲,配合 PreStop 45 秒的 K8s 配置,才做到无损发布。

七、最佳实践总结

  1. 按业务隔离池:订单、推送、日志分开,互相不可见;
  2. 监控线程池状态:暴露 ThreadPoolExecutor 的 activeCount/queueSize/rejectedCount 到 Prometheus + Grafana;
  3. 拒绝策略选型:对延迟敏感用 AbortPolicy + 重试补偿,对完成性要求高用 CallerRunsPolicy;
  4. 异常必须兜底:注册 AsyncUncaughtExceptionHandler + 告警;
  5. MDC 传递不可少:否则线上排查像无头苍蝇;
  6. 优雅关闭是底线:waitForTasks + awaitTermination + PreStop 三重保障。