Spring Boot 使用DeferredResult实现长轮询

2,107 阅读3分钟

文章目录

前言

异步支持是在Servlet 3.0中引入的,简单来说,它允许在请求接收器线程之外的另一个线程中处理HTTP请求。

从Spring 3.2开始可用的DeferredResult有助于将长时间运行的计算从http-worker线程卸载到单独的线程。

尽管另一个线程将占用一些资源来进行计算,但同时不会阻止工作线程,并且可以处理传入的客户端请求。

异步请求处理模型非常有用,因为它有助于在高负载期间很好地扩展应用程序,尤其是对于IO密集型操作。

使用DeferredResult 进行的同步和异步通信,还将比较异步如何更好地扩展以应对高负载和IO密集型用例。

1. 阻塞的REST服务

让我们从开发标准的阻塞REST服务开始:

@GetMapping("/process-blocking")
public ResponseEntity<?> handleReqSync(Model model) { 
    // ...
    return ResponseEntity.ok("ok");
}

这里的问题是请求处理线程被阻塞,直到处理完完整的请求并返回结果为止。对于长时间运行的计算,这是次优的解决方案。

2. 使用DeferredResult的非阻塞REST

为了避免阻塞,我们将使用基于回调的编程模型,在该模型中,我们将DeferredResult返回到servlet容器,而不是实际结果 。

@GetMapping("/async-deferredresult")
public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) {
    LOG.info("Received async-deferredresult request");
    DeferredResult<ResponseEntity<?>> output = new DeferredResult<>();
    
    ForkJoinPool.commonPool().submit(() -> {
        LOG.info("Processing in separate thread");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
        }
        output.setResult(ResponseEntity.ok("ok"));
    });
    
    LOG.info("servlet thread freed");
    return output;
}

请求处理在单独的线程中完成,一旦完成,我们将对DeferredResult对象调用setResult操作。

让我们看一下日志输出,以检查我们的线程是否按预期运行:

[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Received async-deferredresult request
[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Servlet thread freed
[nio-8080-exec-6] java.lang.Thread : Processing in separate thread

在内部,将通知容器线程,并将HTTP响应传递到客户端。容器(servlet 3.0或更高版本)将保持打开连接,直到响应到达或超时。

3. DeferredResult回调

我们可以使用DeferredResult注册三种回调类型:完成,超时和错误回调。

让我们使用*onCompletion()*方法定义异步请求完成时执行的代码块:

deferredResult.onCompletion(() -> LOG.info("Processing complete"));

同样,我们可以使用onTimeout()注册自定义代码以在发生超时时调用。为了限制请求处理时间,我们可以在DeferredResult对象创建过程中传递一个超时值 :

DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>(500l);

deferredResult.onTimeout(() -> 
  deferredResult.setErrorResult(
    ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
      .body("Request timeout occurred.")));

如果发生超时,我们将通过DeferredResult注册的超时处理程序来设置不同的响应状态。

让我们通过处理一个超过5秒的定义超时值的请求来触发超时错误:

ForkJoinPool.commonPool().submit(() -> {
    LOG.info("Processing in separate thread");
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        ...
    }
    deferredResult.setResult(ResponseEntity.ok("OK")));
});

让我们看一下日志:

[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
servlet thread freed
[nio-8080-exec-6] java.lang.Thread: Processing in separate thread
[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
Request timeout occurred

在某些情况下,由于某些错误或异常长时间运行的计算将失败。在这种情况下,我们还可以注册一个*onError()*回调:

deferredResult.onError((Throwable t) -> {
    deferredResult.setErrorResult(
      ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body("An error occurred."));
});

如果发生错误,在计算响应时,我们将通过此错误处理程序设置不同的响应状态和消息正文。


🍎QQ群【837324215】
🍎关注我的公众号【Java大厂面试官】,一起学习呗🍎🍎🍎