异步处理:Callable/DeferredResult/WebAsyncTask

同步接口里,Servlet 容器线程会一直占着等结果——如果业务要等第三方接口或跑批,线程就浪费在等待上。异步处理让容器线程先归还,等结果好了再由容器回调完成响应。

为什么需要异步

同步模型:1 个请求占用 1 个容器线程直到响应结束。慢任务多了线程池被占满,Tomcat 默认 200 线程很容易被打穿。异步模型:处理器启动一个「后台任务」后立刻返回,容器线程释放去接新请求;后台任务完成后容器再次派发(ASYNC dispatch)把结果写回。

Spring MVC 提供三种由浅入深的写法,前提是 Servlet 3.0+ 且容器/过滤器开启 async-supported。

Callable:最省事的异步

方法直接返回 Callable,框架用任务执行器去跑:

@RestController
public class AsyncApi {
    @GetMapping("/api/slow")
    public Callable<String> slow() {
        return () -> {
            Thread.sleep(3000);          // 模拟慢业务
            return "done after 3s";
        };
    }
}
// 请求进来立刻返回(容器线程释放),3 秒后容器再派发写出 "done after 3s"

默认执行器是 SimpleAsyncTaskExecutor(来一个任务开一个线程,生产不建议直接用),超时与执行器在 WebMvcConfigurer 中统一配置:

@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    configurer.setDefaultTimeout(10_000);                        // 10 秒超时(毫秒)
    configurer.setTaskExecutor(new ThreadPoolTaskExecutor());    // 换成有界线程池
}

DeferredResult:完全掌控完成时机

DeferredResult 更像「票据」:方法立刻返回一个空壳,业务代码在任意线程里调用 setResult 兑现它:

@RestController
public class TaskApi {
    private final Map<String, DeferredResult<String>> pending = new ConcurrentHashMap<>();

    @GetMapping("/api/task/start")
    public DeferredResult<String> start(@RequestParam String taskId) {
        DeferredResult<String> result = new DeferredResult<>(5000L); // 5 秒超时
        result.onTimeout(() -> result.setErrorResult("timeout"));    // 超时回调
        pending.put(taskId, result);
        return result;                        // 先返回,不阻塞容器线程
    }

    // 模拟其他线程(消息队列、调度任务)完成业务后兑现
    public void finish(String taskId, String payload) {
        DeferredResult<String> result = pending.remove(taskId);
        if (result != null) {
            result.setResult(payload);        // 容器收到后派发写出响应
        }
    }
}

典型场景:长轮询、消息推送、任务状态查询——任务在别处跑,接口「挂」到结果上。

WebAsyncTask:Callable + 超时/回调管理

WebAsyncTask 给 Callable 加上了超时与完成回调:

@GetMapping("/api/web-async")
public WebAsyncTask<String> webAsync() {
    Callable<String> callable = () -> {
        Thread.sleep(2000);
        return "ok";
    };
    WebAsyncTask<String> task = new WebAsyncTask<>(3000L, callable); // 3 秒超时
    task.onTimeout(() -> "服务器繁忙,请稍后重试");                    // 超时兜底文案
    return task;
}

三种方式对比

方式特点适合
Callable写法最接近同步,框架代跑把同步慢逻辑异步化的快速改造
DeferredResult结果由任意线程 setResult长轮询、跨系统回调、任务队列
WebAsyncTaskCallable + 超时/回调需要超时兜底的 Callable

注意事项

  • 异步只解决「等待时不占线程」,不加快业务本身;秒杀/下载等场景还要配合队列限流。
  • 配了拦截器时,异步派发阶段 preHandle/postHandle 的调用次数与同步不同(存在并发开始回调),行为以官方文档为准。
  • 过滤器若未开启 async-supported 会拦截不到二次派发或直接抛异常。

异步三板斧:容器线程「先还回去」、后台任务慢慢跑、结果就绪容器「再回来」写响应。简单慢逻辑用 Callable,需要外部线程兑现用 DeferredResult,要超时保护用 WebAsyncTask。

笔记加载中…