背景
使用异步 servlet 主要原因就是因为,在 service 方法中业务逻辑如果碰到 io 操作时间比较长的操作,这样这个 service 方法就会长时间占用 tomcat 容器线程池中的线程,这样是不利于其他请求的处理的,当线程池中的线程处理任务时,任务由于长时间 io 操作,肯定会阻塞线程处理其他任务,引入异步 servlet 的目的就是将容器线程池和业务线程池分离开。在处理大 io 的业务操作的时候,把这个操作移动到业务线程池中进行,释放容器线程,使得容器线程处理其他任务,在业务逻辑执行完毕之后,再通知 tomcat 容器线程池来继续后面的操作,这个操作应该是把处理结果 commit 到客户端或者是 dispatch 到其他 servlet 上。原始模型在处理业务逻辑的过程中会一直占有容器线程池,而异步 servlet 模型,在业务线程池处理的过程中,有一段时间容器线程池中的那个线程是空闲的,这种设计大大提高了容器的处理请求的能力
DeferredResult 和 Callable 都是为了异步生成返回值提供基本的支持。简单来说就是一个请求进来,如果你使用了 DeferredResult 或者 Callable,在没有得到返回数据之前,DispatcherServlet 和所有 Filter 就会退出 Servlet 容器线程并释放其资源,同时也允许容器去处理其它请求。但响应保持打开状态,一旦返回数据有了,这个 DispatcherServlet 就会被再次调用并且处理,以异步产生的方式,向请求端返回值。 这么做的好处就是请求不会长时间占用服务连接池,提高服务器的吞吐量
Callable
Callable 的实现比较简单,call() 方法的返回值就是服务端返回给请求端的数据
@RestController
public class AsyncCallableController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final TaskService taskService;
@Autowired
public AsyncCallableController(TaskService taskService) {
this.taskService = taskService;
}
@RequestMapping(value = "/callable", method = RequestMethod.GET, produces = "text/html")
public Callable<String> executeSlowTask() {
logger.info("Request received");
Callable<String> callable = taskService::execute;
logger.info("Servlet thread released");
return callable;
}
}
返回 Callable 意味着 Spring MVC 将调用在不同的线程中执行定义的任务。Spring 将使用 TaskExecutor 来管理线程。在等待完成的长期任务之前,servlet 线程将被释放
[nio-8080-exec-5] x.s.w.c.AsyncCallableController : Request received
[nio-8080-exec-5] x.s.w.c.AsyncCallableController : Servlet thread released
[ MvcAsync2] x.spring.web.service.TaskServiceImpl : Slow task executed
返回 Callable 对象时,实际工作线程会在后台处理,Controller 无需等待工作线程处理完成,可以服务于其他请求。但 Spring 会在工作线程处理完毕后才返回客户端
执行流程:
- 客户端请求服务
- SpringMVC 调用 Controller,Controller 返回一个 Callback 对象
- SpringMVC 调用 request.startAsync 并且将 Callback 提交到 TaskExecutor 中去执行
- DispatcherServlet 以及 Filters 等从应用服务器线程中结束,但 Response 仍旧是打开状态,也就是说暂时还不返回给客户端
- TaskExecutor 调用 Callback 返回一个结果,SpringMVC 将请求发送给应用服务器继续处理
- DispatcherServlet 再次被调用并且继续处理 Callback 返回的对象,最终将其返回给客户端
DeferredResult
DeferredResult 的处理过程与 Callback 类似,不一样的地方在于它的结果不是DeferredResult 直接返回的,而是由其它线程通过同步的方式设置到该对象中
该类包含以下日常使用相关的特性:
超时配置:通过构造函数可以传入超时时间,单位为毫秒;因为需要等待设置结果后才能继续处理并返回客户端,如果一直等待会导致客户端一直无响应,因此必须有相应的超时机制来避免这个问题;实际上就算不设置这个超时时间,应用服务器或者 Spring 也会有一些默认的超时机制来处理这个问题
结果设置:它的结果存储在一个名称为 result 的属性中;可以通过调用 setResult 的方法来设置属性;由于这个 DeferredResult 天生就是使用在多线程环境中的,因此对这个 result 属性的读写是有加锁的
一旦启用了异步请求处理功能,控制器就可以将返回值包装在 DeferredResult,控制器可以从不同的线程异步产生返回值。优点就是可以实现两个完全不相干的线程间的通信
@RestController
public class AsyncDeferredController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final TaskService taskService;
@Autowired
public AsyncDeferredController(TaskService taskService) {
this.taskService = taskService;
}
@RequestMapping(value = "/deferred", method = RequestMethod.GET, produces = "text/html")
public DeferredResult<String> executeSlowTask() {
logger.info("Request received");
DeferredResult<String> deferredResult = new DeferredResult<>();
CompletableFuture.supplyAsync(taskService::execute)
.whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
logger.info("Servlet thread released");
return deferredResult;
}
}
和 Callable 不同的是这一次线程是由我们管理。创建一个线程并将结果 set 到 DeferredResult 是由我们自己来做的
[io-8080-exec-10] x.s.w.c.AsyncDeferredController : Request received
[io-8080-exec-10] x.s.w.c.AsyncDeferredController : Servlet thread released
[onPool-worker-1] x.spring.web.service.TaskServiceImpl : Slow task executed
执行流程:
- 客户端请求服务
- SpringMVC 调用 Controller,Controller 返回一个 DeferredResult 对象
- SpringMVC 调用 request.startAsync
- DispatcherServlet 以及 Filters 等从应用服务器线程中结束,但 Response 仍旧是打开状态,也就是说暂时还不返回给客户端
- 某些其它线程将结果设置到 DeferredResult 中,SpringMVC 将请求发送给应用服务器继续处理
- DispatcherServlet 再次被调用并且继续处理 DeferredResult 中的结果,最终将其返回给客户端
Callable 和 DeferredResult 区别
- DeferredResult 更灵活,可以主动 setResult 到 DeferredResult 中并返回,实现两个完全不相干的线程间的通信
- Callable 是由 SpringMVC 管理异步线程,而 DeferredResult 是自己创建线程处理结果