在编写 DataShard 组件时,偶然发现一个上下文传递变量丢失问题
@GetMapping("/user/page")
public Mono<Page<UserDO>> pageUser() {
CompletableFuture<Page<UserDO>> future = CompletableFuture.supplyAsync(() -> demoMapper.findPage(1),
Ttl.wrap(Executors.newCachedThreadPool()));
return Mono.fromFuture(future);
}
Question:大家可以思考一下,当我在Tomcat运行以上代码时,从开始请求到响应结束🔚 一共有多少个线程参与?(这里假设demoMapper.findPage方法中不会fork多余的线程)
一共三个线程,分别是:
- Tomcat请求线程 http-nio-18080-exec-3@12303
- CachedThreadPool工作线程 pool-12-thread-1
- Tomcat响应线程 http-nio-18080-exec-3@12304
如果答案跟你猜想的不一样,那么可以接着往下看,为什么会产生这几个线程
返回Mono之后Spring如何处理
一般在Controller中执行完成,当我们返回结果之后,会经过org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
方法
并且在方法的最后会执行org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue
来处理返回的参数
在这里我们返回的值类型为MonoCompletionStage(Mono的子类),我们继续跟进
Mono的流程可能是异步可能是同步获取结果,在Spring中并不能完全归类于Async,因此此处判断返回值类型为非异步,
通过org.springframework.web.method.support.HandlerMethodReturnValueHandler#supportsReturnType
方法我们可以拿到处理Mono的Handler(org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler
)
该类最终会将我们的returnValue交由ReactiveTypeHandler处理,从命名可以看出ReactiveTypeHandler是专门处理Reactor逻辑的处理器,通过内置的org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler.DeferredResultSubscriber
去subscribe MonoCompletionStage,获取最终的结果。
我们回到org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler#handleValue
这个方法的末端,当定义DeferredResultSubscriber去subscribe MonoCompletionStage之后,
最终会通过WebAsyncUtils.getAsyncManager(request)获取WebAsyncManager并执行startDeferredResultProcessing流程,startDeferredResultProcessing是专门处理并发异步结果的方法
public void startDeferredResultProcessing(
final DeferredResult<?> deferredResult, Object... processingContext) throws Exception {
Assert.notNull(deferredResult, "DeferredResult must not be null");
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
...
// 前面都是在组装asyncWebRequest,和一些拦截器逻辑
// 这一步主要是组装AsyncContext,并执行AsyncContext的startAsync方法(关于该方法的解释如下AsyncContext.startAsync)
startAsyncProcessing(processingContext);
try {
interceptorChain.applyPreProcess(this.asyncWebRequest, deferredResult);
// 此处设置回掉逻辑
deferredResult.setResultHandler(result -> {
result = interceptorChain.applyPostProcess(this.asyncWebRequest, deferredResult, result);
// 这里会触发redispatch逻辑,相当于模拟了一次http请求
setConcurrentResultAndDispatch(result);
});
}
catch (Throwable ex) {
setConcurrentResultAndDispatch(ex);
}
}
Servlet 3.0的异步处理支持特性,使Servlet 线程不再需要一直阻塞,直到业务处理完毕才能再输出响应,最后才结束该 Servlet 线程。在接收到请求之后,Servlet 线程可以将耗时的操作委派给另一个线程来完成,自己在不生成响应的情况下返回至容器。针对业务处理较耗时的情况,这将大大减少服务器资源的占用,并且提高并发处理速度
1、传统Servlet处理
Web容器会为每个请求分配一个线程,默认情况下,响应完成前,该线程占用的资源都不会被释放。若有些请求需要长时间(例如长处理时间运算、等待某个资源),就会长时间占用线程所需资源,若这类请求很多,许多线程资源都被长时间占用,会对系统的性能造成负担。
2、新特性:异步处理
Servlet 3.0新增了异步处理,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,原先释放了容器所分配线程的请求,其响应将被延后,可以在处理完成(例如长时间运算完成、所需资源已获得)时再对客户端进行响应。
Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下:
第一步,Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理;
第二步,调用业务接口的某些方法,以完成业务处理;
第三步,根据处理的结果提交响应,Servlet 线程结束。
其中第二步的业务处理通常是最耗时的,这主要体现在数据库操作,以及其它的跨网络调用等,在此过程中,Servlet 线程一直处于阻塞状态,直到业务方法执行完毕。在处理业务的过程中,Servlet 资源一直被占用而得不到释放,对于并发较大的应用,这有可能造成性能的瓶颈。对此,在以前通常是采用私有解决方案来提前结束 Servlet 线程,并及时释放资源。
Servlet 3.0 针对这个问题做了开创性的工作,现在通过使用 Servlet 3.0 的异步处理支持,之前的 Servlet 处理流程可以调整为如下的过程:
第一步,Servlet 接收到请求之后,可能首先需要对请求携带的数据进行一些预处理;
第二步,Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,
第三步,Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用),或者将请求继续转发给其它 Servlet。
Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。
AsyncContext不是让你异步输出,而是让你同步输出,但是解放服务器端的线程使用,使用AsyncContext的时候,对于浏览器来说,他们是同步在等待输出的,但是对于服务器端来说,处理此请求的线程并没有卡在那里等待,则是把当前的处理转为线程池处理了,关键就在于线程池,服务器端会起一个线程池去服务那些需要异步处理的请求,而如果你自己每次请求去起一个线程处理的话,这就有可能会耗大量的线程。
执行回掉逻辑后的处理流程
以上流程执行完成之后此时请求线程被回收,等待异步流程执行完成之后的回调,因此该阶段是从Tomcat请求线程 http-nio-18080-exec-3@12303过渡到CachedThreadPool工作线程 pool-12-thread-1
当异步流程执行完成之后回调org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler.DeferredResultSubscriber#onComplete
更新result
回调org.springframework.web.context.request.async.StandardServletAsyncWebRequest#onComplete设置asyncCompleted
为true
并触发·org.springframework.web.context.request.async.WebAsyncManager#setConcurrentResultAndDispatch·进行re-dispatch操作并设置dispatch对象(本质上是一个Runnable对象,具体逻辑见·org.apache.catalina.core.AsyncContextImpl.AsyncRunnable#run·)
private void setConcurrentResultAndDispatch(Object result) {
synchronized (WebAsyncManager.this) {
if (this.concurrentResult != RESULT_NONE) {
return;
}
this.concurrentResult = result;
}
// 因为在回掉org.springframework.web.context.request.async.StandardServletAsyncWebRequest#onComplete设置asyncCompleted为true
// 因此这边会直接通过走到下面的dispatch逻辑
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async result set but request already complete: " + formatRequestUri());
}
return;
}
if (logger.isDebugEnabled()) {
boolean isError = result instanceof Throwable;
logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri());
}
// 本质上就是执行AsyncContextImpl的dispatch逻辑模拟了一次http请求
this.asyncWebRequest.dispatch();
}
ReDispatch的处理流程
执行完上述回调逻辑之后,org.apache.coyote.AbstractProcessor#dispatch
将会收到re-dispatch的request,最终执行org.apache.catalina.core.AsyncContextImpl#doInternalDispatch
,
本质上是执行org.apache.catalina.core.AsyncContextImpl.AsyncRunnable#run
,该方法触发了org.apache.catalina.AsyncDispatcher#dispatch
,后面就是大家熟悉的一套org.springframework.web.servlet.DispatcherServlet#doDispatch
,
至于怎么拿到上一次回掉的result可以去参考org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
逻辑,这里就不多赘述。
该阶段则顺利从CachedThreadPool工作线程 pool-12-thread-1过渡到Tomcat响应线程 http-nio-18080-exec-3@12304