Spring基础学习-SpringMVC异步处理模式分析(DeferredResult/SseEmitter等)

1. 背景

Tomcat等应用服务器的连接线程池实际上是有限制的;每一个连接请求都会耗掉线程池的一个连接数;如果某些耗时很长的操作,如对大量数据的查询操作、调用外部系统提供的服务以及一些IO密集型操作等,会占用连接很长时间,这个时候这个连接就无法被释放而被其它请求重用。如果连接占用过多,服务器就很可能无法及时响应每个请求;极端情况下如果将线程池中的所有连接耗尽,服务器将长时间无法向外提供服务!

在常规场景中,客户端需要等待服务器处理完毕后返回才能继续进行其它操作,这个场景下每一步都是同步调用,如客户端调用Servlet后需要等待其处理返回,Servlet调用具体的Controller后也需要等待其返回。这种情况是在服务器端开发中最常见的场景,适合于服务器端处理时间不是很长的情况;默认情况下Spring的Controller提供的就是这样的服务。

当某项服务处理时间过长时,如邮件发送,需要调用到外部接口,处理时间不受调用方的控制,因此如果耗时过长会有两个比较严重的后果:一是如上文所说的会长时间的占用请求连接数,严重时有可能导致服务器失去响应; 二是客户端等待时间过长,导致前端应用的用户友好性下降,而且客户很有可能因为长时间得不到服务器响应而重复操作,从而加重服务器的负担,使得应用崩溃的机率变大!

为应对这种场景,一般会启用一个后台的线程池,处理请求的Controller会先提交一个耗时长操作如邮件发送到线程池中,然后立即返回到前台。因此处理响应的主线程耗时变短,客户感受到的就是在点击某个发送按钮后很快就得到服务器反馈结果,然后就放心的继续处理其它工作。实际上邮件发送这种事情延迟几秒对于客户来说根本感受不到。当然应用需要保证提交到线程池中的任务执行成功,或者是执行失败后在前端某个地方能够看到失败的具体情况。

这种场景在Spring中可使用TaskExecutor或者是Async来处理,关于它们的用法请参考:Spring基础学习-任务执行(TaskExecutor及Async)

通过以上两种场景,很容易就会想到,如果某个操作既耗时很长,客户端又必须要等待其返回才能进一步处理时,应该通过什么方式来处理?Servlet3.0中引入异步请求处理来处理这种场景,相应的,Spring在3.2版本中就引入相关机制来使用Servlet的该特性。

2. SpringMVC异步处理概述

为满足耗时任务占用应用服务器连接数,而客户端又必须等待这些耗时长任务返回才能处理下一步工作的场景,Spring引入了以下机制来处理:

    使用Callable或者DeferredResult当成Controller的返回值,能够处理异步返回单个结果的场景

    使用ResponseBodyEmitter/SseEmitter或者StreamingResponseBody来流式处理多个返回值

    在Controller中使用响应式客户端调用服务并返回响应式的数据对象

2.1 Callable

Callable直接使用在Controller中被RequestMapping所注解的方法上,做为其返回对象。

使用示例:

@RequestMapping("/testCallable")

public Callable<String> testCallable() {

    logger.info("Controller开始执行!");

    Callable<String> callable = () -> {

        Thread.sleep(5000);

        logger.info("实际工作执行完成!");

        return "succeed!";

    };

    logger.info("Controller执行结束!");

    return callable;

}

使用浏览器访问http://localhost/test/testCallable, 结果如下:

2018-03-12 22:38:05.547  INFO 4980 --- [p-nio-80-exec-2] c.l.t.b.e.controllers.TestController    : Controller开始执行!

2018-03-12 22:38:05.553  INFO 4980 --- [p-nio-80-exec-2] c.l.t.b.e.controllers.TestController    : Controller执行结束!

2018-03-12 22:38:10.560  INFO 4980 --- [      MvcAsync1] c.l.t.b.e.controllers.TestController    : 实际工作执行完成!

可以看到以下结果:

    浏览器等待了大约5秒后返回结果

    打印日志中,Controller在6ms就执行结束

    打印日志中,实际的任务执行在一个名称为MvcAsync1的线程中执行,并且在Controller执行完5s后才执行结束

因此可以得到结论:

    返回Callable对象时,实际工作线程会在后台处理,Controller无需等待工作线程处理完成,但Spring会在工作线程处理完毕后才返回客户端。

    它的执行流程是这样的:

    客户端请求服务

    SpringMVC调用Controller,Controller返回一个Callback对象

    SpringMVC调用ruquest.startAsync并且将Callback提交到TaskExecutor中去执行

    DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端

    TaskExecutor调用Callback返回一个结果,SpringMVC将请求发送给应用服务器继续处理

    DispatcherServlet再次被调用并且继续处理Callback返回的对象,最终将其返回给客户端

2.2 DeferredResult

DeferredResult使用方式与Callable类似,但在返回结果上不一样,它返回的时候实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。

该类包含以下日常使用相关的特性:

    超时配置:通过构造函数可以传入超时时间,单位为毫秒;因为需要等待设置结果后才能继续处理并返回客户端,如果一直等待会导致客户端一直无响应,因此必须有相应的超时机制来避免这个问题;实际上就算不设置这个超时时间,应用服务器或者Spring也会有一些默认的超时机制来处理这个问题。

    结果设置:它的结果存储在一个名称为result的属性中;可以通过调用setResult的方法来设置属性;由于这个DeferredResult天生就是使用在多线程环境中的,因此对这个result属性的读写是有加锁的。

接下来将对DeferredResult的处理流程进行说明,并实现一个较为简单的示例。

2.2.1 DeferredResult处理流程

DeferredResult的处理过程与Callback类似,不一样的地方在于它的结果不是DeferredResult直接返回的,而是由其它线程通过同步的方式设置到该对象中。它的执行过程如下所示:

    客户端请求服务

    SpringMVC调用Controller,Controller返回一个DeferredResult对象

    SpringMVC调用ruquest.startAsync

    DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端

    某些其它线程将结果设置到DeferredResult中,SpringMVC将请求发送给应用服务器继续处理

    DispatcherServlet再次被调用并且继续处理DeferredResult中的结果,最终将其返回给客户端

2.2.2 DeferredResult使用示例

本示例将在一个Controller中添加两个RequestMapping注解的方法。其中一个返回的是DeferredResult的对象,另外一个设置这个对象的值。

@RestController

@RequestMapping("/test")

public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @Autowired

    private AsyncService asyncService;

    private DeferredResult<String> deferredResult = new DeferredResult<>();

    /**

    * 返回DeferredResult对象

    *

    * @return

    */

    @RequestMapping("/testDeferredResult")

    public DeferredResult<String> testDeferredResult() {

        return deferredResult;

    }

    /**

    * 对DeferredResult的结果进行设置

    * @return

    */

    @RequestMapping("/setDeferredResult")

    public String setDeferredResult() {

        deferredResult.setResult("Test result!");

        return "succeed";

    }

}


第一步先访问:http://localhost/test/testDeferredResult

此时客户端将会一直等待,直到一定时长后会超时

第二步再新开页面访问:http://localhost/test/setDeferredResult

此时第一个页面会返回结果。

2.3 SseEmitter

Callback和DeferredResult用于设置单个结果,如果有多个结果需要返回给客户端时,可以使用SseEmitter以及ResponseBodyEmitter等;

下面直接看示例,与DeferredResult的示例类似:

@RestController

@RequestMapping("/test")

public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @Autowired

    private AsyncService asyncService;

    private DeferredResult<String> deferredResult = new DeferredResult<>();

    private SseEmitter sseEmitter = new SseEmitter();

    /**

    * 返回SseEmitter对象 

    *

    * @return

    */

    @RequestMapping("/testSseEmitter")

    public SseEmitter testSseEmitter() {

        return sseEmitter;

    }

    /**

    * 向SseEmitter对象发送数据 

    *

    * @return

    */

    @RequestMapping("/setSseEmitter")

    public String setSseEmitter() {

        try {

            sseEmitter.send(System.currentTimeMillis());

        } catch (IOException e) {

            logger.error("IOException!", e);

            return "error"; 

        }

        return "Succeed!";

    }

    /**

    * 将SseEmitter对象设置成完成

    *

    * @return

    */

    @RequestMapping("/completeSseEmitter")

    public String completeSseEmitter() {

        sseEmitter.complete();

        return "Succeed!";

    }

}


第一步访问:http://localhost/test/testSseEmitter

第二步连续访问:http://localhost/test/setSseEmitter

第三步访问:http://localhost/test/completeSseEmitter

可以看到结果,只有当第三步执行后,第一步的访问才算结束。

2.4 StreamingResponseBody

用于直接将结果写出到Response的OutputStream中; 如文件下载等,示例:

@GetMapping("/download")

public StreamingResponseBody handle() {

    return new StreamingResponseBody() {

        @Override

        public void writeTo(OutputStream outputStream) throws IOException {

            // write...

        }

    };

}


3 异步处理拦截器

在进行异步处理时,可以使用CallableProcessingInterceptor来对Callback返回参数的情况进行拦截,也可以使用DeferredResultProcessingInterceptor来对DeferredResult的情况进行拦截。 也可以直接使用AsyncHandlerInterceptor 。

拦截器的使用与普通拦截器并无不一样的,因此此处不再展开。具体可以参考: Spring Boot拦截器示例及源码原理分析

参考资料

---------------------

作者:icarusliu81

来源:CSDN

原文:https://blog.csdn.net/icarusliu/article/details/79539105

版权声明:本文为博主原创文章,转载请附上博文链接!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容

  • 16. Web MVC 框架 16.1 Spring Web MVC 框架介绍 Spring Web 模型-视图-...
    此鱼不得水阅读 998评论 0 4
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,411评论 1 14
  • 1.Spring web mvc介绍 Spring web mvc和Struts2都属于表现层的框架,它是Spri...
    七弦桐语阅读 11,503评论 2 38
  • 今天我想和自己来一场对话。也是一个记录,一个心态改变的记录。 自生病以来,你就一直在家,因为需要生存,你干起了微商...
    大老爷的不老书阅读 221评论 1 0