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工作线程,并发承载能力更高,实现步骤如下:
- 定义线程安全的容器存储待处理请求,容器必须是单例级别(可以直接写在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; - 改造
/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; } - 改造
/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
相关产品推荐
相关产品推荐

