我们常规的完成这个业务,一般这样操作,伪代码如下
代码语言:javascript复制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就会被再次调用并且处理,以异步产生的方式,向请求端返回值。这么做的好处就是请求不会长时间占用服务连接池,提高服务器的吞吐量
- 1、采用callable方式
可以看到以下结果:
- 浏览器等待了大约5秒后返回结果
- 打印日志中,Controller在6ms就执行结束
- 打印日志中,实际的任务执行在一个名称为MvcAsync1的线程中执行,并且在Controller执行完2s后才执行结束
我们注意一下日志,上面有一段警告。意思就是没有指定线程池。会导致使用默认的SimpleAsyncTaskExecutor发现不停的在创建MvcAsync1这个线程,我就在想,难道没有用线程池?通过阅读WebAsyncManager源码才发现果真如此,WebAsyncManager是Spring MVC管理async processing的中心类。
默认是使用SimpleAsyncTaskExecutor,这个会为每次请求创建一个新的线程
代码语言:javascript复制private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(this.getClass().getSimpleName());
如果说任务指定了executor,就用任务指定的,没有就用默认的SimpleAsyncTaskExecutor
代码语言:javascript复制AsyncTaskExecutor executor = webAsyncTask.getExecutor();
if (executor != null) { this.taskExecutor = executor; }
我们可以配置async 的线程池,不需要为每个任务单独指定
因此可以得到结论:
返回Callable对象时,实际工作线程会在后台处理,Controller无需等待工作线程处理完成,但Spring会在工作线程处理完毕后才返回客户端。
它的执行流程是这样的:
- 客户端请求服务
- SpringMVC调用Controller,Controller返回一个Callback对象
- SpringMVC调用ruquest.startAsync并且将Callback提交到TaskExecutor中去执行
- DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端
- TaskExecutor调用Callback返回一个结果,SpringMVC将请求发送给应用服务器继续处理
- DispatcherServlet再次被调用并且继续处理Callback返回的对象,最终将其返回给客户端
2、DeferredResult方式
DeferredResult使用方式与Callable类似,但在返回结果上不一样,它返回的时候实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。
该类包含以下日常使用相关的特性:
- 超时配置:通过构造函数可以传入超时时间,单位为毫秒;因为需要等待设置结果后才能继续处理并返回客户端,如果一直等待会导致客户端一直无响应,因此必须有相应的超时机制来避免这个问题;实际上就算不设置这个超时时间,应用服务器或者Spring也会有一些默认的超时机制来处理这个问题。
- 结果设置:它的结果存储在一个名称为result的属性中;可以通过调用setResult的方法来设置属性;由于这个DeferredResult天生就是使用在多线程环境中的,因此对这个result属性的读写是有加锁的。
接下来将对DeferredResult的处理流程进行说明,并实现一个较为简单的示例。
DeferredResult的处理过程与Callback类似,不一样的地方在于它的结果不是DeferredResult直接返回的,而是由其它线程通过同步的方式设置到该对象中。它的执行过程如下所示:
- 客户端请求服务
- SpringMVC调用Controller,Controller返回一个DeferredResult对象
- SpringMVC调用ruquest.startAsync
- DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端
- 某些其它线程将结果设置到DeferredResult中,SpringMVC将请求发送给应用服务器继续处理
- DispatcherServlet再次被调用并且继续处理DeferredResult中的结果,最终将其返回给客户端
第一步先访问: http://localhost:8080/testDeferredResult
此时客户端将会一直等待,直到一定时长后会超时
第二步再新开页面访问: http://localhost:8080/setDeferredResult
此时第一个页面会返回结果。
Callback和DeferredResult用于设置单个结果。如果有多个结果需要返回给客户端时,可以使用SseEmitter以及ResponseBodyEmitter等;
下面直接看示例,与DeferredResult的示例类似:
第一步访问: http://localhost:8080/testSseEmitter 一直等待结果
第二步连续访问: http://localhost:8080/setSseEmitter
第三步访问: http://localhost:8080/completeSseEmitter
只有当第三步执行后,第一步才可以看到结果,第一步的访问才算结束。
作者:老顾聊技术 来源:https://www.toutiao.com/a6664747403435835912/