Springboot如何实现异步非阻塞数据响应

1,477 阅读6分钟

Springboot如何实现异步非阻塞数据响应

1. 什么是异步响应?

通俗一点讲就是不是同一个线程完成请求到响应的流程就是异步响应(话外音:其实也不一定如果使用同一个线程池也可能是同一个线程),当请求进入web容器后,tomcat会select到请求io就绪状态,随后由Poller线程交给tomcat线程池中的线程去完成,但是如果整个流程均由tomcat线程完成会严重影响请求的并发效率,所以当tomcat线程只负责请求后快速返回线程池那么本次请求为异步响应。

2. 异步响应的好处

主要是提高web容器的并发,不会出现同步阻塞tomcat线程,等待响应后再处理下一个请求,影响并发处理速度 (话外音:当并发超过tomcat最大线程后会进入队列等待,超过队列大小会被拒绝) ,此时并发模型为异步非阻塞。

3. 源码分析

controller的返回值使用Callable包装后跟踪源码看看tomcat和spingboot分别做了什么,为了方便查看响应时与请求不是一个线程 (也可能不是) , 搭配Aop或拦截器打印结束时的线程名称即可。

 @GetMapping("/xx")
    public Callable<String> callable(HttpServletRequest httpServletRequest){
        System.out.println("请求线程开始" + Thread.currentThread().getName());
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(1000);
                System.out.println("执行完成");
                return "id";
            }
        };
        System.out.println("请求线程结束");
        return callable;
    }

在NioEndpoint类中的run方法中tomcat会不断的select key查看是否有事件就绪,当我们调用接口时,select发现有事件已经就绪,会将每个就绪的socket包装为 NioSocketWrapper 对象并调用 processSocket方法

    processSocket(socketWrapper, SocketEvent.OPEN_READ, true)

在processSocket方法中将后续流程交给tomcat线程池中的线程去执行 SocketProcessorBase对象中的doRun 方法

           SocketProcessorBase<S> sc = null;
            if (processorCache != null) {
                sc = processorCache.pop();
            }
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                sc.reset(socketWrapper, event);
            }
            // 此时的线程为Poller线程
            Executor executor = getExecutor();
            if (dispatch && executor != null) {
             // 此时的线程为Tomcat线程
                executor.execute(sc);
            } else {
                sc.run();
            }
 SocketProcessor(doRun方法具体实现) - 继承 >SocketProcessorBase(抽象) - 实现 > Runnable

在SocketProcessor的dorun方法中会继续调用如下方法

     //handler接口只有ConnectionHandler一种实现,所以就是调用ConnectionHandler的process方法
     getHandler().process(socketWrapper, event);
     

由于我们开始传入的event为OPEN_READ所以将会调用service方法

else if (status == SocketEvent.OPEN_READ) {
                state = service(socketWrapper);
        }

在service方法中会调用getAdapter().service方法并传入request&response,到这其实就应该有点眼熟了想想servlet的service、doGet、doPost

    //Adapter接口只有CoyoteAdapter一种实现,所以就是调用CoyoteAdapter的service方法
   getAdapter().service(request, response);

然后经过一系列眼熟的doFilter操作后,看到关键的servlet,当前的servlet是HttpServlet,进入HttpServlet中的还会调用service方法此时有若干个实现类,由于我们是spirngboot所以进入了FrameworkServlet实现类中

 //所以本质上调用的就是FrameworkServlet重写的service方法
  servlet.service(request, response);

到这就是根据HttpMethod走FrameworkServlet自己重写的doGet、doPost等方法

protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
             //此处省略部分源码...
             doGet(req, resp);
        } else if (method.equals(METHOD_HEAD)) {
            //此处省略部分源码...
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);

        } else {
           //此处省略部分源码...
                  }

无论哪种类型都有部分类似的逻辑所以都会调用processRequest,看到Dispatch方法就放心了因为SpringMvc的Servlet实现就是DispatchServlet,而FrameworkServlet是DispatcherServlet 的父类

doService(request, response) ->  this.doDispatch(request, response) -> this.doDispatch(request, response);

进入doDispatch方法后一直向下会进入invokeHandlerMethod方法() 此处有说法,默认是空的不会进入if块中 什么时候会进入呢?看完后面就知道了

 if (asyncManager.hasConcurrentResult()) {
                result = asyncManager.getConcurrentResult();
                mavContainer = (ModelAndViewContainer)asyncManager.getConcurrentResultContext()[0];
                asyncManager.clearConcurrentResult();
                LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
                    String formatted = LogFormatUtils.formatValue(result, !traceOn);
                    return "Resume with async result [" + formatted + "]";
                });
                invocableMethod = invocableMethod.wrapConcurrentResult(result);
            }
//此时进入的方法
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

中间略过部分逻辑,一直向下invoke之后最终会调用method.invoke(this.getBean(), args)方法获得当前请求controller方法的返回值,此时controller方法已经出栈

//False
 return KotlinDetector.isSuspendingFunction(method) ? CoroutinesUtils.invokeSuspendingFunction(method, this.getBean(), args) : method.invoke(this.getBean(), args);

如上述所将此时返回的是一个未准备就绪的Callable,如果是常规类型则直接获取到返回值,然后就是对获取到的返回值进行处理

//处理返回值  this.returnValueHandlers.handleReturnValue(returnValue,this.getReturnValueType(returnValue), mavContainer, webRequest);
  
---
//根据返回值类型获取具体实现
 HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
            //具体有哪些处理类型查看handler的实现类有很多种
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }

由于我们是Callable返回值所以走的是CallableMethodReturnValueHandler的处理器 (画外音:从这里好戏才刚刚开始) ,处理器中调用startCallableProcessing -> startAsyncProcessing

  //此方法中使用了Servlet3.0新特性 放心的去百度 AsyncContext
  this.startAsyncProcessing(processingContext);
  
  //如果你百度了的话会发现似乎有些眼熟
  this.asyncContext = this.getRequest().startAsync(this.getRequest(),this.getResponse());
  this.asyncContext.addListener(this); 
  

此时asyncContext被初始化完成,并将后续流程交给另外一个线程去处理,然后最开始进来的Tomcat线程任务结束光荣回归到Tomcat线程池中,此时线程变为Task线程池中的线程

  try {
            Future<?> future = this.taskExecutor.submit(() -> {
                Object result = null;

                try {
                    //省略部分代码...
                    //本质就是去帮你执行controller中new Callable的call方法
                    result = callable.call();
                } catch (Throwable var8) {
                    result = var8;
                } finally {
                    result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, result);
                }

                this.setConcurrentResultAndDispatch(result);
            });
            interceptorChain.setTaskFuture(future);


当异步线程Task同步执行完call方法后调用this.setConcurrentResultAndDispatch,将返回值赋值给this.concurrentResult

this.setConcurrentResultAndDispatch(result);
//异步线程Task将call返回值写回到concurrentResult
//这个参数就是上面IF判断时使用的,但是上面逻辑不是已经执行完了吗,没错还会再调用一次这时就会走IF
this.concurrentResult = result;

接下来会调用asyncContext的dispatch方法

  this.dispatch = new AsyncRunnable(
                    request, applicationDispatcher, servletRequest, servletResponse);
  //里面调用                
  this.request.getCoyoteRequest().action(ActionCode.ASYNC_DISPATCH, null);
 protected void processSocketEvent(SocketEvent event, boolean dispatch) {
        SocketWrapperBase<?> socketWrapper = getSocketWrapper();
        if (socketWrapper != null) {
            //processSocket(ActionCode.ASYNC_DISPATCH, null) 眼熟吗?  
            socketWrapper.processSocket(event, dispatch);
        }
    }

最终会回到这个地方再次走一遍流程只不过concurrentResult有值了

 try {
            if (socketWrapper == null) {
                return false;
            }
            SocketProcessorBase<S> sc = null;
            if (processorCache != null) {
                sc = processorCache.pop();
            }
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                sc.reset(socketWrapper, event);
            }
            Executor executor = getExecutor();
            if (dispatch && executor != null) {
                //Task线程回收,重新启用一个Tomcat线程去执行后续逻辑
                //如果你加了Aop或拦截器,那么打印的线程名可能是不同的(画外音:同一个线程池有概率复用调用时的线程)
                executor.execute(sc);
            } else {
                sc.run();
            }

4. 总结

整个过程经历了三次线程(线程池无额外开销), 大致流程就是 客户端请求->tomcat nio select 发现就绪事件 -> 交给Tomcat线程 -> 交给Task线程池执行call方法 (此时tomcat线程已经回收,并不是wait或者block状态) -> 当执行完毕后再次从Tomcat线程池取一个线程去执行将返回值写给客户端

整个流程是异步且非阻塞的,高效使用了tomcat线程,tomcat线程的快速回收且非阻塞能够提高整体并发量,这里只分析了Callable方式可以根据handler.handleReturnValue重写方法查看其他的异步处理方式如 Deferred