关于java:DeferredResult-如何实现长轮询

64次阅读

共计 5540 个字符,预计需要花费 14 分钟才能阅读完成。

明天咱们就来盘一下 Spring DeferredResult,这玩意应用起来是很简略的,答复的话可深可浅。

浅一点就是间接答复它的作用,深一点的话就是原理了,而这个原理其实还须要波及到 tomcat(默认 tomcat 为 web 容器)。

咱们先来看下 DeferredResult 的作用及应用形式。

DeferredResult 的用途

DeferredResult 其实是基于 Servlet 3.0 对异步申请的反对而来的,咱们来看这样一个场景:

以后 controller 外面有个办法 A,其外部逻辑依赖 redis 外面的一个值,如果 redis 外面有值了,就能够获取返回,如果没值这时候没有货色能够返回,只能返回 null,而往 redis 塞入值依赖另一个后盾线程。

失常的实现咱们必定能够想到轮询的计划,即浏览器一直轮询办法 A,直到有值才进行轮询,然而有时候过于频繁的轮询会给服务器产生压力。

而这时候 DeferredResult 就能够退场啦,从名字咱们就能够晓得:延期的后果

咱们来简略的看下应用形式:

能够看到,只须要用 DeferredResult 包装原本须要返回的值,而后设置一个超时工夫和超时兜底值即可,例子设置了 5s。

咱们来试验一下,如果后盾线程就睡了 100 ms,未超过设置的超时工夫,此时过了 100 ms 就立马返回值:

如果后盾线程睡了 10000 ms,那么超过设置的超时工夫,此时期待了 5 秒后,返回的是:

能够看到,这性能合乎咱们的预期。

对于 DeferredResult  还有一个很重要的点:申请的解决线程(即 tomcat 线程池的线程)不会等到 DeferredResult#setResult() 被调用才开释,而是间接开释了

也就是说 tomcat 线程安顿好 DeferredResult 的一些配置后,不会等逻辑解决完(DeferredResult#setResult()的调用或者超时)。

而是间接开释了,这样 tomcat 线程就被回收到线程池中了,能够响应其余申请,不会傻傻地阻塞等着 DeferredResult#setResult() 被调用或超时。

咱们都晓得 tomcat 的线程池大小是无限的,如果咱们的一些业务逻辑解决慢的话,会慢慢地占满 tomcat 线程,这样就无奈解决新的申请,所以一些解决迟缓的业务咱们会放到业务线程池中解决,但单纯的放到业务线程池中解决的话,咱们无奈得悉其什么时候解决完,也无奈将解决完的后果和之前的申请匹配上,所以常做的形式就是轮询。

而 DeferredResult 的做法就相似仅把事件安顿好,不会管事件做好没,tomcat 线程就开释走了,留神此时不会给申请方(如浏览器)任何响应,而是将申请寄存在一边,咱先不论它,等前面有后果了再把之前的申请拿来,把值响应给申请方。

最初还有个兜底的解决,如果超时了还没把该返回的值塞进来,那么就响应申请方兜底的值。

我来画个图(留神这不是源码剖析图,仅仅只是为了更好地了解 DeferredResult 的逻辑):

看到这里想必你应该明确 DeferredResult 的用法以及它的一些个性了吧?

接下来咱们看下原理。

DeferredResult 原理剖析

这个原理说来话长,真要齐全了解的话须要同时懂得 Tomcat 的原理和 SpringMvc 的原理,为了防止适度开展偏离主题,本文次要针对 DeferredResult 的原理。

前面有工夫再补充残缺 Tomcat 和 SpringMvc 的原理,有须要的能够评论区留个言,多的话我立马安顿。

我打算次要用文字来形容整体的过程,会跟一些源码(如果全上源码的话我怕会看晕了,毕竟调用链路还是有点长的)

咱们做到了解就行,真对源码有趣味的能够自行钻研

开始剖析

一个申请先要通过 tomcat 的调度,而后才会到咱们的 Spring 中来,待咱们在 Spring 中解决完业务逻辑后,tomcat 才会把相干的响应返回给客户端(如浏览器)。

所以想要实现 DeferredResult 的性能,须要 tomcat 的配合,它须要知道这是一个异步申请,在后果还未设置且没超时之前,不能将响应返回给客户端,待 DeferredResult 塞入值之后,再将申请返回给客户端。

我再贴一下示例代码,咱们基于这个代码剖析:

如果你用浏览器申请调用这个办法,那么 tomcat 此时必定不晓得这是一个异步解决的申请,只会认为是失常申请进行解决。

所以失常的话是 tomcat 调用 SpringMvc 定义的 DispatcherServlet#doDispatch 来解决申请。

依据 url 找到 controller 解决的办法即上文的实例办法。失常解决的话,这个办法应该给浏览器间接返回咱们 new 的那个 deferredResult,但显然方才演示的后果并不是这样。

那两头产生了什么事呢?

这就波及到 SpringMvc 的内容。当通过反射调用 controller 中的办法失去返回值的时候,须要依据返回值的类型调用不同的 returnValueHandlers来解决。

先来解释下为什么须要 returnValueHandlers 来解决返回值。

你想想看,如果咱们返回类型可能是视图名或者是被标注了 @RequestBody的值,这两者的解决形式必定不一样,所以须要对返回值解决下。

而 Spring 搞了个 returnValueHandler 是专门用来解决 DeferredResult  类型的返回值,即 DeferredResultMethodReturnValueHandler

这里就能够做一些操作了,能够看到反对解决的返回类型是 DeferredResult:

从这里咱们能够得悉,对于 DeferredResult  返回类型其实前半部的解决和失常的返回值没有区别,区别就在于对办法的返回值做了非凡解决

我简要地说下这个 returnValueHandler 触发的操作:

它调用了 tomcat 外面 Request#startAsync 办法,也传递了 timeout 的工夫,这个操作是让 tomcat 明确以后申请是一个异步申请,这样 tomcat 就不会间接将 new 的那个 deferredResult 返回给客户端,也不会销毁以后的 request 和 response。

而是会将解决这个申请的 Processor 临时保存起来(简略的了解为每个申请都有对应的一个 processor,这是 tomcat 外面的概念),放到 waitingProcessors 外面。

而后 tomcat 线程池就溜了,解决完了,不论了。

此时上半部的筹备工作就做完了,前面有两种解决门路:

  • 一种是 timeout 了
  • 一种是在超时前调用了 DeferredResult#setResult 胜利返回。

咱们别离来看看两种的不同:

申请超时了

从后面咱们曾经得悉,异步申请曾经被放到 waitingProcessors 里,且超时工夫也曾经设置上了,而 tomcat 会有一个线程,每隔 1 秒遍历 waitingProcessors 外面的 processor,看看它们过期了没:

如果发现过期了,那么会从新往 tomcat 外面线程池外面投掷工作,然而这个工作不太一样,能够看到 SocketEvent 是 TIMEOUT

这样线程池跑到这个工作的时候就晓得这个曾经超时申请工作,此时就会将超时值塞入到申请中(具体是通过之前设置的 DeferredResult 相干的 interceptor 中的 handleTimeout 办法)

这个 setResultInteral 最终就会将值保留到申请中(实际上是申请治理的 asyncManager 里),并将该申请从新进入 DispatcherServlet#doDispatch() 中解决。

你看这个申请又走了一遍 doDispatch,所以 DeferredResult 类型的办法会走两遍 doDispatch 逻辑,第二次走进来的时候发现申请外面的 asyncManager 曾经被放入值了。

因为曾经塞了值,所以前面解决的时候,就会走到这个 if 外面,这外面就会把原先 controller 外面办法的反射内容替换了,新建一个反射内容,这个新建的反射办法的调用是间接返回塞入的 result。

看到没,这就是所谓的偷梁换柱,这样还是复用了失常的解决流程,只不过前面执行的时候取得的是被塞入的超时值,且因为是失常类型,那么该怎么解决就怎么解决,最终返回给了浏览器。

申请未超时,DeferredResult#setResult

其实,DeferredResult#setResult 和申请超时的解决逻辑是截然不同的,差异就是触发的起源不同

申请超时是 tomcat 线程池扫描到超时,而后通过 handleTimeout 调用 setResultInternal将超时默认值塞入后从新触发 DispatcherServlet#doDispatch()  解决。

而 DeferredResult#setResult 是咱们利用线程被动塞入值:

实际上也是调用 setResultInternal,而后从新触发 DispatcherServlet#doDispatch()  解决。

所以两者前面的解决逻辑截然不同!

补充

其实外面有很多源码和比拟重要的点都没提及,次要是开展的话太多了,这里略微带一下。

比方 WebAsyncManager,这玩意就如其名是一个异步的管理器,每个申请都会 new 一个与之配对,下面也有提到一点点。

失常申请都只会进入一次到 doDispatch 办法中,而异步的 DeferredResult 进入了两次,这就要保障第二次进入的时候跟第一次进入是同一个 WebAsyncManager,一些信息须要缓存对齐,并且外面还有异步解决的逻辑,比方设置值从新触发 doDispatch 等等。

还有 tomcat 的申请其实有个状态机在,这里就须要理解 tomcat 的相干概念。

例如开始异步解决的时候会调用:

这个 action 底层就会调用 asyncStateMachine 进行操作:

而后这种状态机最终是告诉到 CoyoteAdapter 的,这也是 tomcat 外面的一个概念。

tomcat 的连接器是通过调用 CoyoteAdapter#service 来调用容器的,这外面有异步申请的解决

public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
            throws Exception {

        ... 省略局部代码....

        try {
            // Parse and set Catalina and configuration specific
            // request parameters
            postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                // Calling the container 调用容器
                connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
            }
            if (request.isAsync()) { // 这里就是通过状态机判断的
                async = true;
                ..... 省略局部代码....
            } else {
      // 如果不是异步就完结了
                request.finishRequest(); 
                response.finishResponse();}

        } catch (IOException e) {// Ignore} finally {
            .... 省略局部代码....
            if (!async) {updateWrapperErrorCount(request, response);
                // 不是异步就回收 request 和 response 了
                request.recycle();
                response.recycle();}
        }
    }

最初

大略理解到这个水平差不多了,如果想要深刻理解还是须要把握挺多内容的。

用简略的话来总结下 Spring DeferredResult:如果返回值类型是 DeferredResult  则表明其是异步申请,tomcat 线程不会等到利用程序处理完或者超时,而是会立刻开释线程。

而这个未解决完的申请则会暂存,tomcat 通晓其为异步申请,也不会对客户端进行响应,直至 tomcat 线程扫描到申请超时或者利用线程将 result 塞入到 DeferredResult 中。

次要原理就是 Spring 有个DeferredResultMethodReturnValueHandler,辨认到返回值是 DeferredResult  类型的话,会将这个申请标记为异步申请(这是基于 tomcat 对 servlet 3.0 异步申请的反对),且暂存这个申请。

待 tomcat 每秒扫描期待的异步申请是否超时来触发是否返回默认值,或者利用线程手动塞值到 DeferredResult  触发返回,具体返回的逻辑其实是二次利用 Spring 的 DispatcherServlet#doDispatch,进行再次散发。

利用一个 WebAsyncManager 对象与申请进行绑定,进行异步操作的治理,如返回值的保留,上下文的保留等。

通过 WebAsyncManager  状态的不同 DispatcherServlet#doDispatch解决逻辑有所不同,判断其实异步申请、判断其是否曾经有异步返回值等。

如果有了返回值则通过新建一个反射内容替换之前 request 对应的 controller 的办法,通过偷梁换柱的形式替换反射返回的后果。

好了,就这样差不多了。

后面我也提了,这其实波及到 tomcat 的底层原理,而后还有 SpringMvc 的原理,说实话作为面试题不错,能够延长挺多的~

正文完
 0