JAVA开发中这种异步场景如何提升系统的吞吐量?

原创 码农  2019-12-19 10:40:31  阅读 437 次 评论 0 条

前言

降低延迟和提高吞吐量对应的方法有两种: 优化算法和将机器的硬件性能发挥到极致。

优化算法就是降低时间和空间复杂度,使得程序执行时间更短。

而将硬件的性能发挥到极致,具体的指提高I/O 和cpu的利用率。

那么,针对下面的异步场景,该如何优化和提升吞吐量呢?


先看下图,一个请求做注册用户(A业务)耗时1秒,在由A调用B业务(发送邮件)耗时3秒,以及C业务(发送短信)耗时2秒

JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第1张


我们常规的完成这个业务,一般这样操作,伪代码如下

public String doA(){
 //......
 //A业务完成
 //调用B业务
 doB(相关参数);
 //调用C业务
 doC(相关参数);
 //返回值
 return a;
}


这种方式doA方法得到返回值需要的时间为6000ms = 1000ms(A业务耗时) + 3000ms(B业务耗时) + 2000ms(C业务耗时)

还有一点很多文章里面都没有提到,那就是线程的处理方式。doA(),doB(),doC()都是由同一个线程Thead1处理的;这种方式我们通常会称为同步操作


优化方案


很多人看到此场景,想到的方案就是异步处理,就是B和C业务用其他线程处理,这样整个请求只要耗时1秒,也就是处理完A业务后,就返回了。B和C业务由后台执行

但有些业务是不能这么做的,如doA的返回值,一定需要知道B或C业务的处理结果,也就是一定返回相关的B或C业务结果。有同学就会说,那不就是同步方案吗?前端浏览器等待所有业务执行完。

话说的没有错,但这种同步方案中,有个很大的问题,系统吞吐量不高。

我们的应用部署到tomcat中,tomcat可以支持并发100个请求线程,那在处理A业务的时候,需要6秒;在此6秒内也就只能支持100个请求。我们如何提高吞吐量呢?我们可以采用分解的方式,在A业务完成后重新分配系统线程处理B和C业务,等待B和C业务处理后在返回给前端。

前端得到返回值(6000ms) = 1000ms(A业务耗时,tomcat线程Thread1) + 3000ms(B业务耗时,线程Thread2) + 2000ms(C业务耗时,线程Thread3);虽然前端得到返回值耗时还是6秒,但A业务容器线程执行完业务就立刻归还线程给tomcat容器,他可以继续处理其他的请求。A业务会创建副线程进行B和C业务的处理。这样的话请求A业务tomcat可以达到1秒内支持100个请求,6秒内能达到600个请求,提供了6倍。这样就极大的提升了系统吞吐量。

这种方案spring给我们提供了DeferredResult和Callable方式实现。

技术方案

官方文档中说DeferredResult和Callable都是为了异步生成返回值提供基本的支持。简单来说就是一个请求进来,如果你使用了DeferredResult或者Callable,在没有得到返回数据之前,DispatcherServlet和所有Filter就会退出Servlet容器线程,但响应保持打开状态,一旦返回数据有了,这个DispatcherServlet就会被再次调用并且处理,以异步产生的方式,向请求端返回值。 这么做的好处就是请求不会长时间占用服务连接池,提高服务器的吞吐量


采用callable方式

JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第2张


JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第3张


可以看到以下结果:

  • 浏览器等待了大约5秒后返回结果

  • 打印日志中,Controller在6ms就执行结束

  • 打印日志中,实际的任务执行在一个名称为MvcAsync1的线程中执行,并且在Controller执行完2s后才执行结束

我们注意一下日志,上面有一段警告。意思就是没有指定线程池。会导致使用默认的SimpleAsyncTaskExecutor,发现不停的在创建MvcAsync1这个线程。

我就在想,难道没有用线程池?通过阅读WebAsyncManager源码才发现果真如此,WebAsyncManager是Spring MVC管理async processing的中心类。

默认是使用SimpleAsyncTaskExecutor,这个会为每次请求创建一个新的线程

private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(this.getClass().getSimpleName());


源码如下,如任务指定了executor,就用任务指定的,没有就用默认的SimpleAsyncTaskExecutor

AsyncTaskExecutor executor = webAsyncTask.getExecutor();
if (executor != null) {
  this.taskExecutor = executor;
}


我们可以配置async 的线程池,不需要为每个任务单独指定

JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第4张


JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第5张


因此可以得到结论:

返回Callable对象时,实际工作线程会在后台处理,Controller无需等待工作线程处理完成,但Spring会在工作线程处理完毕后才返回客户端。

它的执行流程是这样的:

  • 客户端请求服务

  • SpringMVC调用Controller,Controller返回一个Callback对象

  • SpringMVC调用ruquest.startAsync并且将Callback提交到TaskExecutor中去执行

  • DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端

  • TaskExecutor调用Callback返回一个结果,SpringMVC将请求发送给应用服务器继续处理

  • DispatcherServlet再次被调用并且继续处理Callback返回的对象,最终将其返回给客户端

DeferredResult方式


DeferredResult使用方式与Callable类似,但在返回结果上不一样,它返回的时候实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。

该类包含以下日常使用相关的特性:

  • 超时配置:通过构造函数可以传入超时时间,单位为毫秒;因为需要等待设置结果后才能继续处理并返回客户端,如果一直等待会导致客户端一直无响应,因此必须有相应的超时机制来避免这个问题;实际上就算不设置这个超时时间,应用服务器或者Spring也会有一些默认的超时机制来处理这个问题。

  • 结果设置:它的结果存储在一个名称为result的属性中;可以通过调用setResult的方法来设置属性;由于这个DeferredResult天生就是使用在多线程环境中的,因此对这个result属性的读写是有加锁的。

DeferredResult流程


接下来将对DeferredResult的处理流程进行说明,并实现一个较为简单的示例。

DeferredResult的处理过程与Callback类似,不一样的地方在于它的结果不是DeferredResult直接返回的,而是由其它线程通过同步的方式设置到该对象中。它的执行过程如下所示:

  • 客户端请求服务

  • SpringMVC调用Controller,Controller返回一个DeferredResult对象

  • SpringMVC调用ruquest.startAsync

  • DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端

  • 某些其它线程将结果设置到DeferredResult中,SpringMVC将请求发送给应用服务器继续处理

  • DispatcherServlet再次被调用并且继续处理DeferredResult中的结果,最终将其返回给客户端

JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第6张


第一步先访问:http://localhost:8080/testDeferredResult

此时客户端将会一直等待,直到一定时长后会超时

第二步再新开页面访问:http://localhost:8080/setDeferredResult

此时第一个页面会返回结果。

Callback和DeferredResult用于设置单个结果。如果有多个结果需要返回给客户端时,可以使用SseEmitter以及ResponseBodyEmitter等;

下面直接看示例,与DeferredResult的示例类似:

JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第7张


JAVA开发中这种异步场景如何提升系统的吞吐量? 架构 第8张


第一步访问:http://localhost:8080/testSseEmitter 一直等待结果

第二步连续访问:http://localhost:8080/setSseEmitter

第三步访问:http://localhost:8080/completeSseEmitter

只有当第三步执行后,第一步才可以看到结果,第一步的访问才算结束。

本文地址:https://www.itcodeit.com/post/23.html
版权声明:本文为原创文章,版权归 码农 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

还没有留言,还不快点抢沙发?