【小家Spring】高性能关键技术之---体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult) 基础使用篇(上)

简介: 【小家Spring】高性能关键技术之---体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult) 基础使用篇(上)

前言


现在已经在2019年,这个时候再来谈Spring MVC的异步模式,好像有点老掉牙了。毕竟现在都Spring5的时代了,甚至将来肯定是webflux的天下了。


而Spring MVC的异步请求模式是Spring3.2就推出了,它是基于基Servlet3.0规范实现的,而此规范是2011年推出的,距现在已经有近10的历史了,可谓是非常非常成熟的一种技术规范了。


但是震惊的是,我前端时间一连问了公司的3位同事(工作5年以上),对Spring MVC的异步模式三缄其口,说不出个所以然,更有连Servlet3.0规范都没听过,有什么新特性都没有了解的。着实让我大吃了一惊~


需要说明的一点:我问的这几位同事,做业务方便绝对是杠杠的没问题的,也有很长的Spring MVC使用经验


我想了一下出现这现象的原因:

1、Spring MVC足够优秀,封装得我们现在处理业务请求只需要面向JavaBean去编程即可,没必要再去了解Servlet底层的细节

2、Servlet源生的API在Spring MVC的环境下,使用场景已经非常的少了。甚至给我们一种错觉:servlet技术已经淡化了大众的视野,是不是都不更新了呢?(显然不是的嘛~毕竟4.0的规范都快出来了)

3、Spring MVC的异步模式多多少少都会增加使用的复杂度,从而增加犯错的概率。而它的同步模式可以说是能够满足现在绝大部分的使用场景(大不了觉得性能不够了,就加机器嘛,很少会从代码的本身去考虑和优化性能),所以确实没使用过也是在清理之中的~


Spring 5


这里小插曲不得不简单说一下Spring5。2016 年7月28日重磅发布Spring5.0版本。

Spring Framework 5.0的**最大特点之一是响应式编程(Reactive Programming)。**它使用了如下规范:


  • Servlet 3.1
  • JMS 2.0
  • JPA 2.1
  • JAX-RS 2.0
  • Bean Validation 1.1


这里必须再提一次它最重要的新特性:响应式编程(Reactive Programming)。为了将下来更好的去学习和深入理解响应式编程的核心内容,我觉得先铺垫此篇文章的讲解尤为重要。


Spring5.0以后,它对servlet不再强依赖,而是变为了可选依赖。另外一个选择还可以是:Reactive编程

Spring MVC的同步模式


要知道什么是异步模式,就先要知道什么是同步模式。


浏览器发起请求,Web服务器开一个线程处理(请求处理线程),处理完把处理结果返回浏览器。这就是同步模式。,绝大多数Web服务器都如此般处理。这里面有几个关键的点:简单示例图如下

image.png


此处需要明晰一个概念:比如tomcat,它既是一个web服务器,同时它也是个servlet后端容器(调ava后端服务),所以要区分清楚这两个概念。请求处理线程是有限的,宝贵的资源~(注意它和处理线程的区别)


1.请求发起者发起一个request,然后会一直等待一个response,这期间它是阻塞的


2.请求处理线程会在Call了之后等待Return,自身处于阻塞状态(这个很关键)


3.然后都等待return,知道处理线程全部完事后返回了,然后把response反给调用者就算全部结束了


问题在哪里?


绝大部分情况下,这样是没有问题的。因为

第一:高并发、高流量的场景放眼中国的公司,占比也是非常少的。

第二:长时间处理服务这种情况也是少之又少的


所以两者结合起来,场景就更加稀少了。相信这就是为什么好多做开发N年了的,却还不知道Servlet和Spring MVC的异步模式的原因吧。


正所谓,别人不懂的地方,咱们才有机会嘛。因此好好学习本文的内容,能让你升值哦~

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


Spring MVC异步模式Demo Show


Spring MVC3.2之后支持异步请求,能够在controller中返回一个Callable或者DeferredResult。由于Spring MVC的良好封装,异步功能使用起来出奇的简单。


Callable案例:

  @Controller
  @RequestMapping("/async/controller")
  public class AsyncHelloController {
      @ResponseBody
      @GetMapping("/hello")
      public Callable<String> helloGet() throws Exception {
          System.out.println(Thread.currentThread().getName() + " 主线程start");
          Callable<String> callable = () -> {
              System.out.println(Thread.currentThread().getName() + " 子子子线程start");
              TimeUnit.SECONDS.sleep(5); //模拟处理业务逻辑,话费了5秒钟
              System.out.println(Thread.currentThread().getName() + " 子子子线程end");
        // 这里稍微小细节一下:最终返回的不是Callable对象,而是它里面的内容
              return "hello world";
          };
          System.out.println(Thread.currentThread().getName() + " 主线程end");
          return callable;
      }
  }


输出:

http-apr-8080-exec-3 主线程start
http-apr-8080-exec-3 主线程end
MvcAsync1 子子子线程start
MvcAsync1 子子子线程end


先明细两个概念:


  1. 请求处理线程:处理线程 属于 web 服务器线程,负责 处理用户请求,采用 线程池 管理。
  2. 异步线程:异步线程 属于 用户自定义的线程,可采用 线程池管理。


前端页面等待5秒出现结果。


注意:异步模式对前端来说,是无感知的,这是后端的一种技术。所以这个和我们自己开启一个线程处理,立马返回给前端是有非常大的不同的,需要注意~


由此我们可以看出,主线程早早就结束了(需要注意,此时还并没有把response返回的,此处一定要注意),真正干事的是子线程(交给TaskExecutor去处理的,后续分析过程中可以看到),它的大致的一个处理流程图可以如下:

image.png


这里能够很直接的看出:我们很大程度上提高了我们请求处理线程的利用率,从而肯定就提高了我们系统的吞吐量。


异步模式处理步骤概述如下:


1.当Controller返回值是Callable的时候


2.Spring就会将Callable交给TaskExecutor去处理(一个隔离的线程池)


3.与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态


4.Callable线程处理完成后,Spring MVC讲请求重新派发给容器**(注意这里的重新派发,和后面讲的拦截器密切相关)**


5.根据Callabel返回结果,继续处理(比如参数绑定、视图解析等等就和之前一样了)~~~~


Spring官方解释如下截图:


image.png

WebAsyncTask案例:


官方有这么一句话,截图给你:

image.png


如果我们需要超时处理的回调或者错误处理的回调,我们可以使用WebAsyncTask代替Callable

实际使用中,我并不建议直接使用Callable ,而是使用Spring提供的WebAsyncTask 代替,它包装了Callable,功能更强大些


@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {
    @ResponseBody
    @GetMapping("/hello")
    public WebAsyncTask<String> helloGet() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 主线程start");
        Callable<String> callable = () -> {
            System.out.println(Thread.currentThread().getName() + " 子子子线程start");
            TimeUnit.SECONDS.sleep(5); //模拟处理业务逻辑,话费了5秒钟
            System.out.println(Thread.currentThread().getName() + " 子子子线程end");
            return "hello world";
        };
        // 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称
        WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, callable);
        // 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的
        webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调"));
        // 这两个返回的内容,最终都会放进response里面去===========
        webAsyncTask.onTimeout(() -> "程序[超时]的回调");
        // 备注:这个是Spring5新增的
        webAsyncTask.onError(() -> "程序[出现异常]的回调");
        System.out.println(Thread.currentThread().getName() + " 主线程end");
        return webAsyncTask;
    }
}


如上,由于我们设置了超时时间为3000ms,而业务处理是5s,所以会执行onTimeout这个回调函数。因此页面是会显示“程序[超时]的回调”这几个字。其执行的过程同Callback。

相关文章
|
28天前
|
Java 调度 开发者
spring的@Scheduled()有几种定时模式?
【10月更文挑战第12天】spring的@Scheduled()有几种定时模式?
68 1
|
1月前
|
存储 Java API
简单两步,Spring Boot 写死的定时任务也能动态设置:技术干货分享
【10月更文挑战第4天】在Spring Boot开发中,定时任务通常通过@Scheduled注解来实现,这种方式简单直接,但存在一个显著的限制:任务的执行时间或频率在编译时就已经确定,无法在运行时动态调整。然而,在实际工作中,我们往往需要根据业务需求或外部条件的变化来动态调整定时任务的执行计划。本文将分享一个简单两步的解决方案,让你的Spring Boot应用中的定时任务也能动态设置,从而满足更灵活的业务需求。
76 4
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析(五)-策略模式
spring源码设计模式分析(五)-策略模式
|
2月前
|
消息中间件 设计模式 缓存
spring源码设计模式分析(四)-观察者模式
spring源码设计模式分析(四)-观察者模式
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析(六)-模板方法模式
spring源码设计模式分析(六)-模板方法模式
|
2月前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
37 1
|
2月前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析(七)-委派模式
spring源码设计模式分析(七)-委派模式
|
2月前
|
设计模式 Java 数据库
spring源码设计模式分析(八)-访问者模式
spring源码设计模式分析(八)-访问者模式
|
2月前
|
设计模式 搜索推荐 Java
spring源码设计模式分析(三)
spring源码设计模式分析(三)