|
|
51CTO旗下网站
|
|
移动端

大部分程序员不知道的 Servelt3 异步请求,原来这么简单?

想象一下如果业务需要较长时间处理,那么这个 Tomcat 线程其实一直在被占用,随着请求越来越多,可用 I/O 线程越来越少,直到被耗尽。这时后续请求只能等待空闲 Tomcat 线程,这将会加长了请求执行时间。

作者:鸭血粉丝来源:Java极客技术|2020-04-03 08:42

用同步请求模型,所有动作都交给同一个 Tomcat 线程处理,所有动作处理完成,线程才会被释放回线程池。

想象一下如果业务需要较长时间处理,那么这个 Tomcat 线程其实一直在被占用,随着请求越来越多,可用 I/O 线程越来越少,直到被耗尽。这时后续请求只能等待空闲 Tomcat 线程,这将会加长了请求执行时间。

如果客户端不关心返回业务结果,这时我们可以自定义线程池,将请求任务提交给线程池,然后立刻返回。

也可以使用 Spring Async 任务,大家感兴趣可以自行查找一下资料

但是很多场景下,客户端需要处理返回结果,我们没办法使用上面的方案。在 Servlet2 时代,我们没办法优化上面的方案。

不过等到 Servlet3 ,引入异步 Servlet 新特性,可以完美解决上面的需求。

异步 Servlet 执行请求流程:

将请求信息解析为 HttpServletRequest

分发到具体 Servlet 处理,将业务提交给自定义业务线程池,请求立刻返回,Tomcat 线程立刻被释放

当业务线程将任务执行结束,将会将结果转交给 Tomcat 线程

通过 HttpServletResponse 将响应结果返回给等待客户端

引入异步 Servlet3 整体流程如下:

使用异步 Servelt,Tomcat 线程仅仅处理请求解析动作,所有耗时较长的业务操作全部交给业务线程池,所以相比同步请求, Tomcat 线程可以处理 更多请求。

虽然我们将业务处理交给业务线程池异步处理,但是对于客户端来讲,其还在同步等待响应结果。

可能有些同学会觉得异步请求将会获得更快响应时间,其实不是的,相反可能由于引入了更多线程,增加线程上下文切换时间。

虽然没有降低响应时间,但是通过请求异步化带来其他明显优点:

  • 可以处理更高并发连接数,提高系统整体吞吐量
  • 请求解析与业务处理完全分离,职责单一
  • 自定义业务线程池,我们可以更容易对其监控,降级等处理
  • 可以根据不同业务,自定义不同线程池,相互隔离,不用互相影响

所以具体使用过程,我们还需要进行的相应的压测,观察响应时间以及吞吐量等其他指标,综合选择。

异步 Servelt 使用方式

异步 Servelt 使用方式不是很难,阿粉总结就是下面三板斧:

  1. HttpServletRequest#startAsync 获取 AsyncContext 异步上下文对象
  2. 使用自定义的业务线程池处理业务逻辑
  3. 业务线程处理结束,通过 AsyncContext#complete 返回响应结果

下面的例子将会使用 SpringBoot ,Web 容器选择 Tomcat

示例代码如下:

  1. ExecutorService executorService = Executors.newFixedThreadPool(10); 
  2.  
  3. @RequestMapping("/hello"
  4. public void hello(HttpServletRequest request) { 
  5.     AsyncContext asyncContext = request.startAsync(); 
  6.     // 超时时间 
  7.     asyncContext.setTimeout(10000); 
  8.     executorService.submit(() -> { 
  9.         try { 
  10.             // 休眠 5s,模拟业务操作 
  11.             TimeUnit.SECONDS.sleep(5); 
  12.             // 输出响应结果 
  13.             asyncContext.getResponse().getWriter().println("hello world"); 
  14.             log.info("异步线程处理结束"); 
  15.         } catch (Exception e) { 
  16.             e.printStackTrace(); 
  17.         } finally { 
  18.             asyncContext.complete(); 
  19.         } 
  20.     }); 
  21.     log.info("servlet 线程处理结束"); 

浏览器访问该请求将会同步等待 5s 得到输出响应,应用日志输出结果如下:

  1. 2020-03-24 07:27:08.997  INFO 79257 --- [nio-8087-exec-4] com.xxxx   : servlet 线程处理结束 
  2. 2020-03-24 07:27:13.998  INFO 79257 --- [pool-1-thread-3] com.xxxx   : 异步线程处理结束 

这里我们需要注意设置合理的超时时间,防止客户端长时间等待。

SpringMVC

Servlet3 API ,无法使用 SpringMVC 为我们提供的特性,我们需要自己处理响应信息,处理方式相对繁琐。

SpringMVC 3.2 基于 Servelt3 引入异步请求处理方式,我们可以跟使用同步请求一样,方便使用异步请求。

SpringMVC 提供有两种异步方式,只要将 Controller 方法返回值修改下述类即可:

  • DeferredResult
  • Callable

DeferredResult

DeferredResult 是 SpringMVC 3.2 之后引入新的类,只要让请求方法返回DeferredResult,就可以快速使用异步请求,示例代码如下:

  1. ExecutorService executorService = Executors.newFixedThreadPool(10); 
  2.  
  3. @RequestMapping("/hello_v1"
  4. public DeferredResult<String> hello_v1() { 
  5.     // 设置超时时间 
  6.     DeferredResult<String> deferredResult = new DeferredResult<>(7000L); 
  7.     // 异步线程处理结束,将会执行该回调方法 
  8.     deferredResult.onCompletion(() -> { 
  9.         log.info("异步线程处理结束"); 
  10.     }); 
  11.     // 如果异步线程执行时间超过设置超时时间,将会执行该回调方法 
  12.     deferredResult.onTimeout(() -> { 
  13.         log.info("异步线程超时"); 
  14.         // 设置返回结果 
  15.         deferredResult.setErrorResult("timeout error"); 
  16.     }); 
  17.     deferredResult.onError(throwable -> { 
  18.         log.error("异常", throwable); 
  19.         // 设置返回结果 
  20.         deferredResult.setErrorResult("other error"); 
  21.     }); 
  22.     executorService.submit(() -> { 
  23.         try { 
  24.             TimeUnit.SECONDS.sleep(5); 
  25.             deferredResult.setResult("hello_v1"); 
  26.             // 设置返回结果 
  27.         } catch (Exception e) { 
  28.             e.printStackTrace(); 
  29.             // 若异步方法内部异常 
  30.             deferredResult.setErrorResult("error"); 
  31.         } 
  32.     }); 
  33.     log.info("servlet 线程处理结束"); 
  34.     return deferredResult; 
  35.  

创建 DeferredResult 实例时可以传入特定超时时间。另外我们可以设置默认超时时间:

  1. # 异步请求超时时间 
  2. spring.mvc.async.request-timeout=2000 

如果异步程序执行完成,可以调用 DeferredResult#setResult返回响应结果。此时若有设置 DeferredResult#onCompletion 回调方法,将会触发该回调方法。

同时我们还可以设置超时回调方法 DeferredResult#onTimeout,一旦异步线程执行超时,将会触发该回调方法。

最后 DeferredResult 还提供其他异常的回调方法 onError,起初阿粉以为只要异步线程内发生异常,就会触发该回调方法。尝试在异步线程内抛出异常,但是无法成功触发。

后续阿粉查看这个方法的 doc,当 web 容器线程处理异步请求时发生异常,才能成功触发。

 

Callable

Spring 另外还提供一种异步请求使用方式,直接使用 JDK Callable。示例代码如下:

  1. @RequestMapping("/hello_v2"
  2. public Callable<String> hello_v2() { 
  3.     return new Callable<String>() { 
  4.         @Override 
  5.         public String call() throws Exception { 
  6.             TimeUnit.SECONDS.sleep(5); 
  7.             log.info("异步方法结束"); 
  8.             return "hello_v2"
  9.         } 
  10.     }; 

默认情况下,直接执行将会输出 WARN 日志

这是因为默认情况使用 SimpleAsyncTaskExecutor 执行异步请求,每次调用执行都将会新建线程。由于这种方式不复用线程,生产不推荐使用这种方式,所以我们需要使用线程池代替。

我们可以使用如下方式自定义线程池:

  1. @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) 
  2. public AsyncTaskExecutor executor() { 
  3.     ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); 
  4.     threadPoolTaskExecutor.setThreadNamePrefix("test-"); 
  5.     threadPoolTaskExecutor.setCorePoolSize(10); 
  6.     threadPoolTaskExecutor.setMaxPoolSize(20); 
  7.     return threadPoolTaskExecutor; 

注意 Bean 名称一定要是 applicationTaskExecutor,若不一致, Spring 将不会使用自定义线程池。

或者可以直接使用 SpringBoot 配置文件方式配置代替:

  1. # 核心线程数 
  2. spring.task.execution.pool.core-size=10 
  3. # 最大线程数 
  4. spring.task.execution.pool.max-size=20 
  5. # 线程名前缀 
  6. spring.task.execution.thread-name-prefix=test 
  7. # 还有另外一些配置,读者们可以自行配置 

这种方式异步请求的超时时间只能通过配置文件方式配置。

  1. spring.mvc.async.request-timeout=10000 

如果需要为单独请求的配置特定的超时时间,我们需要使用 WebAsyncTask 包装 Callable 。

  1. @RequestMapping("/hello_v3"
  2. public WebAsyncTask<String> hello_v3() { 
  3.     System.out.println("asdas"); 
  4.     Callable<String> callable=new Callable<String>() { 
  5.         @Override 
  6.         public String call() throws Exception { 
  7.             TimeUnit.SECONDS.sleep(5); 
  8.             log.info("异步方法结束"); 
  9.             return "hello_v3"
  10.         } 
  11.     }; 
  12.     // 单位 ms 
  13.     WebAsyncTask<String> webAsyncTask=new WebAsyncTask<>(10000,callable); 
  14.     return webAsyncTask; 

总结

SpringMVC 两种异步请求方式,本质上就是帮我们包装 Servlet3 API ,让我们不用关心具体实现细节。虽然日常使用我们一般会选择使用 SpringMVC 两种异步请求方式,但是我们还是需要了解异步请求实际原理。所以大家如果在使用之前,可以先尝试使用 Servlet3 API 练习,后续再使用 SpringMVC。

  • Referencehttps://www.baeldung.com/spring-deferred-result
  • https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support

【编辑推荐】

  1. Java程序员必备基础:Java代码是怎么运行的?
  2. 一个后端程序员如何被公司逼的开始写前端代码!奉劝各位最好选择前后端分离的公司
  3. 加速函数,每个Python程序员都应该了解标准库的Lru_cache
  4. 迈向全栈开发,程序员必须知道的19个框架和库
  5. 程序员的一次失误,在45分钟里搞垮了一家上市公司
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

庖丁解牛Ceph分布式存储

庖丁解牛Ceph分布式存储

云计算存储的基石
共5章 | Happy云实验室

79人订阅学习

AI语音聊天应用开发

AI语音聊天应用开发

一键部署Web服务
共4章 | jextop

17人订阅学习

 敏捷无敌之 Gitlab CI 持续集成

敏捷无敌之 Gitlab CI 持续集成

打破运维与研发壁垒
共5章 | KaliArch

113人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微