Marco‘s Java【Spring Mono Return 处理流程源码分析】

在编写 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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值