You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot异步请求响应同步门面实现咨询

场景上下文
  • 外部客户端向我的Spring Boot REST端点(/request)发起同步请求
  • 我的REST服务向微服务A发起同步调用
  • 微服务A执行完成后发布事件,随后向我的REST服务返回响应
  • 微服务A发布的事件被微服务B消费,B执行对应事务后,调用我的REST端点/success发送成功标识响应
问题描述

如何实现让/request端点在/success端点被回调触发前保持阻塞等待,待接收到成功通知后再向原始外部客户端返回最终响应?


实现方案

核心逻辑是给每个进入/request的请求生成全局唯一请求ID,通过线程安全的容器关联等待中的请求上下文,/success被回调时匹配到对应请求、写入结果并唤醒等待流程即可,以下是Spring Boot环境下两种可直接落地的实现。

方案1:DeferredResult实现(推荐,适配Spring MVC异步模型,性能最优)

Spring Web原生提供的DeferredResult专门用来处理异步请求响应,不需要手动阻塞Servlet工作线程,并发承载能力更高,实现步骤如下:

  1. 定义线程安全的容器存储待处理请求,容器必须是单例级别(可以直接写在Controller类中,或注册为Spring单例Bean)
    import org.springframework.web.context.request.async.DeferredResult;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.UUID;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    
    // 存储requestId和对应的DeferredResult映射
    private final ConcurrentHashMap<String, DeferredResult<Object>> pendingRequests = new ConcurrentHashMap<>();
    // 自定义请求超时时间,根据业务调整,单位毫秒
    private static final long REQUEST_TIMEOUT = 30000;
    
  2. 改造/request端点逻辑
    @PostMapping("/request")
    public DeferredResult<Object> handleRequest(@RequestBody RequestDto bizParam) {
        // 生成全局唯一请求ID
        String requestId = UUID.randomUUID().toString();
        // 初始化DeferredResult,绑定超时时间
        DeferredResult<Object> deferredResult = new DeferredResult<>(REQUEST_TIMEOUT);
        pendingRequests.put(requestId, deferredResult);
    
        // 注册回调:请求完成、超时、异常时自动清理容器中的记录,防止内存泄漏
        deferredResult.onCompletion(() -> pendingRequests.remove(requestId));
        deferredResult.onTimeout(() -> {
            deferredResult.setErrorResult(ResponseEntity
                    .status(HttpStatus.GATEWAY_TIMEOUT)
                    .body("下游处理超时"));
        });
    
        // 调用微服务A,必须将requestId透传给A,要求A发事件给B时携带该ID,B回调/success时原样传回
        callServiceA(bizParam, requestId);
    
        // 直接返回DeferredResult,Servlet线程不会阻塞,等setResult被调用时才会向客户端写回响应
        return deferredResult;
    }
    
  3. 改造/success回调端点逻辑
    @PostMapping("/success")
    public ResponseEntity<Void> handleSuccessCallback(
            @RequestParam String requestId,
            @RequestBody Object successResult) {
        // 取出对应等待中的请求
        DeferredResult<Object> targetRequest = pendingRequests.remove(requestId);
        if (targetRequest != null) {
            // 写入结果,自动触发响应返回给外部客户端
            targetRequest.setResult(successResult);
        }
        return ResponseEntity.ok().build();
    }
    

方案2:CompletableFuture阻塞等待实现(逻辑直观,适合快速验证)

如果不想用Spring的异步API,也可以用JDK原生的CompletableFuture手动实现等待逻辑,步骤和上面一致,只是容器存储的对象换成CompletableFuture,/request中通过get()方法阻塞等待结果:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

private final ConcurrentHashMap<String, CompletableFuture<Object>> pendingRequests = new ConcurrentHashMap<>();

@PostMapping("/request")
public ResponseEntity<Object> handleRequest(@RequestBody RequestDto bizParam) throws Exception {
    String requestId = UUID.randomUUID().toString();
    CompletableFuture<Object> waitFuture = new CompletableFuture<>();
    pendingRequests.put(requestId, waitFuture);
    try {
        callServiceA(bizParam, requestId);
        // 阻塞等待结果,设置超时防止线程永久挂死
        Object result = waitFuture.get(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS);
        return ResponseEntity.ok(result);
    } catch (TimeoutException e) {
        return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT).body("下游处理超时");
    } finally {
        pendingRequests.remove(requestId);
    }
}

@PostMapping("/success")
public ResponseEntity<Void> handleSuccessCallback(
        @RequestParam String requestId,
        @RequestBody Object successResult) {
    CompletableFuture<Object> waitFuture = pendingRequests.remove(requestId);
    if (waitFuture != null) {
        waitFuture.complete(successResult);
    }
    return ResponseEntity.ok().build();
}

注意:该方案会阻塞Tomcat工作线程,并发量高的时候会占满线程池,不建议生产环境大流量场景使用。

多实例部署适配

如果你的服务是多节点集群部署,上述内存存储方案会出现请求落点不一致的问题:/request落在节点A,/success回调落在节点B,节点B内存中没有对应等待中的请求对象,会导致唤醒失败。可以选两种方案解决:

  • 流量粘滞:在网关层根据requestId做一致性哈希路由,保证同一个requestId的/request和/success请求都落到同一个节点,不需要改造现有逻辑
  • 分布式协调:用Redis发布订阅做中转,所有节点订阅回调事件,/success收到请求后把结果写入Redis并广播事件,持有对应requestId的节点收到事件后完成响应。

内容的提问来源于stack exchange,提问作者Vedant Shetty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:27:13