CompletableFuture + 异步Servlet:同步的姿势、异步的效果

这是一个务实但有效的工具。
假设在Servlet线程模型下的web容器,比如SpringBoot web starter下,
要处理一个慢接口,一般是文件操作、公网第三方接口调用等等,除了提高业务逻辑处理速度提高RT之外,
能做的防守动作是至少不要让这个慢接口拖垮Tomcat的worker线程池。

可供选择的方案可能很快是改技术栈为WebFlux,拥抱Netty; 或者升级老旧的JDK版本到21,直接上虚拟线程、肆意挥霍。
但是,WebFlux代码写法会让一些开发人员不习惯,直接上虚拟线程看似最简单,但是一些情况下也是需要付出一定的精力去确认的,比如:
synchronized 重量级锁 使得JVM没法使用yield使虚拟线程从载体线程卸载,导致虚拟线程与载体线程同时被阻塞。
我们需要确保业务逻辑所使用的第三方库中也不能包含这个关键字,排查起来还是需要一些细心的。

CompletableFuture 配合Servlet3.0异步机制,可以将耗时的业务逻辑转为异步任务,但是整个流程又确保是同步过程,可以收到不错的效果。

Controller有如下慢接口

private final SlowExecutorConfigV2 nnSlowExecutor;

@GetMapping("/nnslow")
public CompletableFuture<ApiResult<Long>> nnslow(@RequestParam Long id) {
    return nnSlowExecutor.submit(() -> mpService.slow(id))
            .orTimeout(15, TimeUnit.SECONDS)
            .thenApply(ApiResult::success);
}

其中mpService.slow(id)预估需要执行几秒到十秒不等
我们上面是把这个逻辑封装成Callable丢给一个独立线程池,执行结果以CompletableFuture形式返回。
这背后,spring mvc 在任务提交到独立线程池的时候就释放worker线程回其线程池,由独立的ThreadPoolExecutor执行业务逻辑,
期间spring mvc保持连接,业务逻辑执行完成由实际干活的异步线程往连接中写CompletableFuture封装的结果。

@Component
public class SlowExecutorConfigV2 {

    private static final Logger LOGGER = LoggerFactory.getLogger(SlowExecutorConfigV2.class);

    /** 工作线程数 */
    private static final int MAX_THREADS = 5;
    /** 同时承载的慢任务上限(工作线程 + 队列) */
    private static final int MAX_CONCURRENT = 30;

    private final ExecutorService pool = new ThreadPoolExecutor(
            MAX_THREADS,
            MAX_THREADS,
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(MAX_CONCURRENT - MAX_THREADS),
            new ThreadFactory() {
                private final AtomicInteger seq = new AtomicInteger(1);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "slow-pool-" + seq.getAndIncrement());
                }
            },
            new ThreadPoolExecutor.AbortPolicy());

    /**
     * 提交慢任务,返回 CompletableFuture 由调用方决定后续处理。
     *
     * 行为约定:
     * - 正常完成:future 以任务返回值完成
     * - 任务抛异常:原样转给 future(全局异常处理器记录真实原因)
     * - 队列满被拒绝:future 以 BusinessException("业务繁忙") 完成,快速失败不堆积
     */
    public <T> CompletableFuture<T> submit(Callable<T> task) {
        CompletableFuture<T> future = new CompletableFuture<>();
        try {
            pool.execute(() -> {
                try {
                    future.complete(task.call());
                } catch (Exception e) {
                    future.completeExceptionally(e);
                }
            });
        } catch (RejectedExecutionException e) {
            LOGGER.error("slow 线程池已满,拒绝任务:{}", e);
            future.completeExceptionally(new BusinessException("Slow业务繁忙"));
        }
        return future;
    }

    /** 容器关闭时停止接收新任务,等待已提交任务执行完,避免非 daemon 线程拖住 JVM 退出 */
    @PreDestroy
    public void shutdown() {
        pool.shutdown();
    }
}

posted on 2026-09-01 16:06  幽州散人  阅读(6)  评论(0)    收藏  举报

导航