[TOC]
Spring - DeferredResult 异步返回实现
@Edit by Typora
Keyword
LongPolling、长轮询、请求异步返回
前言
通常我们经常会遇到一些需要实现异步返回的场景,如长轮询、服务器端处理流程较为复杂并且处理时间比较长的情况,这个时候,如果接受http请求的线程被一直阻塞着,会导致服务器端无法接受处理更多的请求,导致拒绝服务的问题出现,这个时候,将接收请求的线程让出来,会大大提升服务器端并发能力。
Spring在3.2
的版本上就已经为我们提供的相应的机制,以应对Http Nio的场景。
笔者在以下的示例使用的是5.2.9.RELEASE
的版本,请读者使用高于3.2
的版本进行验证。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.web.version}</version>
<scope>provided</scope>
</dependency>
关于DeferredResult
关于DeferredResult,在Spring的类注释上是这样描述的。
DeferredResult provides an alternative to using a Callable for asynchronous request processing. While a Callable is executed concurrently on behalf of the application, with a DeferredResult the application can produce the result from a thread of its choice.
Subclasses can extend this class to easily associate additional data or behavior with the DeferredResult. For example, one might want to associate the user used to create the DeferredResult by extending the class and adding an additional property for the user. In this way, the user could easily be accessed later without the need to use a data structure to do the mapping.
An example of associating additional behavior to this class might be realized by extending the class to implement an additional interface. For example, one might want to implement Comparable so that when the DeferredResult is added to a PriorityQueue it is handled in the correct order.
简要来说就是,提供了一个可替代Callable进行异步请求的方案,接口直接返回DeferredResult,Spring会在值设置进该DeferredResult实例的时候,返回给请求方。
如果读者对Future及其派生类有所了解的话,可以发现,两者有异曲同工之妙,都是等值回来之后进行相应的回调,可以将线程资源释放出来,做其他的事,节约服务器端的线程资源,可以提高服务器端的并发能力。
示例代码
package com.iwuyc.spring.web.demo;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.zcyiot.dm.commons.serializable.GsonUtil;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.Map;
@RestController
@RequestMapping("/longpolling")
public class LongPollingController {
private static final Cache<String, DeferredResult<String>> SETTABLE_FUTURE_LOADING_CACHE = CacheBuilder.newBuilder().build();
/**
* 模拟长轮询的请求
*
* @param requestId 在该用例中,使用该值做请求标识,用于注册、区别DeferredResult实例
* @return 返回DeferredResult实例给Spring框架
*/
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public DeferredResult<String> longPolling(@RequestParam("requestId") String requestId) {
// 设定30秒超时,如果30秒内没有设置值,则直接返回timeoutResult指定的值
DeferredResult<String> result = new DeferredResult<>(30_000L, "{\"error\":\"timeout\"}");
if (null == requestId) {
// 如果缺少RequestId直接返回错误
result.setResult("{\"error\":\"Missing important properties.\"}");
return result;
}
// 将请求的延期返回的值注册到cache中,以便后续流程可以找到相应的实例,进行值设置。
SETTABLE_FUTURE_LOADING_CACHE.put(requestId, result);
// 设置完成的回调函数,以便完成了之后,将该实例从缓存中清除。
result.onCompletion(() -> SETTABLE_FUTURE_LOADING_CACHE.invalidate(requestId));
return result;
}
/**
* 模拟延时响应
* @param body 模拟返回值
* @param requestId 长轮询中的RequestId。
*/
@PostMapping
public void response(@RequestBody Map<?, ?> body, @RequestParam("requestId") String requestId) {
// 根据RequestId找到之前请求的DeferredResult实例
final DeferredResult<String> responseFuture = SETTABLE_FUTURE_LOADING_CACHE.getIfPresent(requestId);
if (null == responseFuture) {
return;
}
// 将值设置到DeferredResult的实例中
responseFuture.setResult(GsonUtil.toJson(body));
}
}
该示例展示了长轮询的场景,方法longPolling模拟长轮询请求,超时时间为30秒。
response方法模拟服务器端处理完结果,并且返回给轮询请求的场景。这种方式只是一种示例,在实际生产中,可能会直接将DeferredResult转交给另外一个线程,进行业务处理,处理完成之后,直接设置返回值,而无需注册到统一的一个缓存中。
长轮询请求示例(对应longPolling方法):
curl --location --request GET 'http://localhost:51001/longpolling?requestId=1' \
--header 'Cookie: hotusm.session.id=fb2dda00-8245-4c03-b19c-9e3842982607; JSESSIONID=8BB1C54884E2ECB04DC2FDA2277B5C83'
相应请求示例(对应response方法):
curl --location --request POST 'http://localhost:51001/longpolling?requestId=1' \
--header 'Content-Type: application/json' \
--header 'Cookie: hotusm.session.id=fb2dda00-8245-4c03-b19c-9e3842982607; JSESSIONID=8BB1C54884E2ECB04DC2FDA2277B5C83' \
--data-raw '{
"name":"Neil",
"address":"深圳"
}'
上面两个示例是该示例的controller的两个请求,可以使用curl进行尝试,也可以转换为相应的postman请求报文,进行模拟请求。