SpringBoot DeferredResult 长轮询实现实现方式?_springboot长轮询_Young丶的博客-程序员秘密

技术标签: 长轮询  spring boot  Spring  DeferredResult  

看过上文《RocketMQ pull push 的长轮询如何实现 ?》 我们了解了长轮询的一些概念。今天我们就来盘一下 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 的原理,说实话作为面试题不错,可以延伸挺多的~

好嘞,后面有时间再写写相关的,今天就说这么多了!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/agonie201218/article/details/123954205

智能推荐

VB可以换背景图片的Form程序_键盘上的舞指的博客-程序员秘密

该VB程序可以进行自定义窗体图片背景或设置纯颜色背景,还可以对图片进行淡化设置和窗体透明度设置等,适合换肤的程序使用。该程序希望对VB初学者有所研究和学习!支持原创!更多VB源码欢迎访问 https://blog.csdn.net/ty5858点击这里下载工程包提取码:FVB2...

es聚合查询与多维度数据统计_es多维度聚合_小码农叔叔的博客-程序员秘密

首先要弄清楚两个概念,聚合与搜索搜索即从一个索引下按照特定的字段或关键词搜索出符合用户预期的一个或者一堆cocument,然后根据文档的相关度得分,在返回的结果集里并根据得分对这些文档进行一定的排序聚合根据业务需求,对文档中的某个或某几个字段进行数据的分组并做一些指标数据的统计分析,比如要计算一批文档中某个业务字段的总数,平均数,最大最小值等,都属于聚合的范畴以上两个概念后是理解下...

前端入门——正则表达式_|绝对值|的博客-程序员秘密

1.正则表达式的创建和使用创建正则表达式的两种方式使用正则表达式字面量const reg = /[a-z]\d+[a-z]/i;优点:简单方便 不需要考虑二次转义缺点:子内容无法重复使用 过长的正则导致可读性差使用 RegExp构造函数const alphabet = '[a-z]'; const reg = new RegExp(`${alphabet}\...

C指针详解[转]_c++指针详解_weixin_41680707的博客-程序员秘密

前言:复杂类型说明    要了解指针,多多少少会出现一些比较复杂的类型,所以我先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单,一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我总结了一下其原则:从变量名处起,根据运算符优先级结合,一步一步分析.下面让我们先从简单的类型开始慢慢分析吧:[cpp] view plain copy int p;...

about percpu_uint(i) < unit(size)_cosmoslhf的博客-程序员秘密

http://www.wowotech.net/linux_kenrel/per-cpu.htmlLinux内核同步机制之(二):Per-CPU变量作者:linuxer 发布于:2014-10-16 11:17 分类:内核同步机制一、源由:为何引入Per-CPU变量? 1、lock bus带来的性能问题 在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持

jvm虚拟机栈的作用_weixin_30407613的博客-程序员秘密

jvm虚拟机栈的作用jvm虚拟机栈栈帧的组成jvm虚拟机栈,也叫java栈,它由一个个的栈帧组成,而栈帖由以下几个部分组成局部变量表-存储方法参数,内部使用的变量操作数栈-在变量进行存储时,需要进行入栈和出栈动态连接-引用类型的指针方法出口-方法的返回一段原程序代码package com.lind.basic;public class Demo1 { stat...

随便推点

Python numpy,scipy,pandas,sklearn 这些库的区别是什么?_Zhang,Xuewen的博客-程序员秘密

Numpy是以矩阵为基础的数学计算模块,纯数学。Scipy基于Numpy,科学计算库,有一些高阶抽象和物理模型。比方说做个傅立叶变换,这是纯数学的,用Numpy;做个滤波器,这属于信号处理模型了,在Scipy里找。Pandas提供了一套名为DataFrame的数据结构,比较契合统计分析中的表结构,并且提供了计算接口,可用Numpy或其它方式进行计算。sklearn 是机器学习的算法库...

robotframework调用python类方法_【RobotFramework】基于Python3的RF自动化测试框架搭建..._weixin_39914752的博客-程序员秘密

Python2.7已于2020年1月1日开始停用,之前RF做自动化都是基于Python2的版本。没办法,跟随时代的脚步,我们也不得不升级以应用新的控件与功能。升级麻烦,直接全新安装。一、Python安装根据操作系统选择对应版本制品下载安装即可,本机用的是Windows x86-64 executable installer。注意事项:安装完成后检查下环境变量,默认会配置好,可以检查下。检测是否安装...

我用PHP图像技术做了一个有趣的贴胡子程序,准确率达到98%_wzm112的博客-程序员秘密

http://blog.jobbole.com/89526/最近微软推出的年龄识别软件可谓是火爆了朋友圈,听说好像是通过识别脸上皱纹来判断年龄的,而我,通过抓取知乎100万用户也小小火了一把,于是我想继续发掘PHP的潜能,看看有没有更多的可能性,做一个有趣的东西出来。别人识别年龄、美颜、变老,我要不贴贴胡子试试?!于是,PHP的贴胡子程序就开始了……要给脸上贴胡子,首先当然得把脸找出来,于是我找到...

Android imageView图片按比例缩放_一叶飘舟的博客-程序员秘密

android:scaleType可控制图片的缩放方式,示例代码如下:android:src="@drawable/logo"android:scaleType="centerInside"android:layout_width="60dip"android:layout_height="60dip"android:layout_centerVertical="true"

Python 学习笔记18-模块_要使用模块中的某一个功能第一步是什么_Maratrix的博客-程序员秘密

什么是模块在 Python 中,一个 .py 文件就是一个模块module。使用模块有什么好处? - 最大的好处是大大提高了代码的可维护性。 - 其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。 - 使用模块还可以避免函数名和变量名冲突。举个例子,一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。包...