zoukankan      html  css  js  c++  java
  • Spring MVC 异步处理请求,提高程序性能

    原文:http://blog.csdn.net/he90227/article/details/52262163

    什么是异步模式

    如何在Spring MVC中使用异步提高性能?
               一个普通 Servlet 的主要工作流程大致如下:
                                           用户查询开始到返回结果到页面,此处是一个同步的过程,如果做成异步的能提高系统响应的性能
                             以下3个步骤都在同一个线程中完成    ---  同步阻塞           ---  请求等待               
                                           首先,Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理;
                                           接着,调用业务接口的某些方法,以完成业务处理;
                                           最后,根据处理的结果提交响应,Servlet 线程结束。
                                           
                                           其中第二步的业务处理通常是最耗时的,这主要体现在数据库操作,以及其它的跨网络调用等,在此过程中,Servlet 线程一直处于阻塞状态,直到业务方法执行完毕,
                                           在处理业务的过程中,请求的连接线程数是有限的,   Servlet 资源一直被占用而得不到释放,对于并发较大的应用,这有可能造成性能的瓶颈
                                           
        
              使用异步处理:
                                          首先,Servlet 接收到请求之后,可能首先需要对请求携带的数据进行一些预处理;
                                          接着,Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器(线程池)
                                          此时 Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用),或者将请求继续转发给其它 Servlet。
                                          如此一来, Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回, Servlet request请求线程可以被快速释放回Web容器,从而提高并发性
       
        Spring MVC 3.2 开始引入Servlet 3中的基于异步的处理request.往常是返回一个值,而现在是一个Controller方法可以返回一个java.util.concurrent.Callable对象和从Spring MVC的托管线程生产返回值.
                同时Servlet容器的主线程退出和释放,允许处理其他请求(处理Http请求的线程是有限的,这里提前释放,可以大大提高并发性)。Spring MVC通过TaskExecutor的帮助调用Callable在一个单独的线程。
                并且当这个Callable返回时,这个rquest被分配回Servlet容器使用由Callable的返回值继续处理。
        
        
        
       Spring MVC 异步调用 原理:
                         Spring返回的Callable被RequestMappingHandlerAdapter拦截,使用SimpleAsyncTaskExecutor线程池处理,         
                         每当任务被提交到此“线程池(这里就交线程池了)”时,线程池产生一个新的线程去执行Callable中的代码,
                         每次都产生新的线程而且没有上上限(默认没有上限的,可以设置concurrencyLimit属性来设置线程数的大小)
                              
                         但:SimpleAsyncTaskExecutor 线程池性能不好,可使用自定义的线程池来代替
                    
                    1.使用ThreadPoolTaskExecutor    ---   该线程池可设置线程大小,有缓冲队列,空闲线程存活时间等
                    2.封装Guava的线程池                                               ---   效率高
                 eg:   
                    ThreadPoolTaskExecutor  ---  org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
                    配置:
             1.设置ThreadPoolTaskExecutor线程池属性
                    <bean id="myThreadPool"  
                    class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">  
                    <property name="corePoolSize" value="5" /><!--最小线程数 -->  
                    <property name="maxPoolSize" value="10" /><!--最大线程数 -->  
                    <property name="queueCapacity" value="50" /><!--缓冲队列大小 -->  
                    <property name="threadNamePrefix" value="abc-" /><!--线程池中产生的线程名字前缀 -->  
                    <property name="keepAliveSeconds" value="30" /><!--线程池中空闲线程的存活时间单位秒 -->  
                   </bean>  
             2.在Spring MVC的 <mvc:annotation-driven>中配置,使用该线程池替换默认的  SimpleAsyncTaskExecutor   线程池
                    <mvc:annotation-driven>  
                    <mvc:async-support task-executor="myThreadPool"  
                        default-timeout="600">  
                        <mvc:callable-interceptors>  
                            <bean class="com.jay.springmvc.async.MyCallableInterceptor" />  
                        </mvc:callable-interceptors>  
                    </mvc:async-support>  
                </mvc:annotation-driven>  
             说明:
                  Callable任务的超时处理   ---  自定义拦截器实现
                  
                  MyCallableInterceptor implements CallableProcessingInterceptor   ---  自定义Callable拦截器 , 可在这里处理Callable任务的超时问题
                   
                    public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task)
                      throws Exception {
                   HttpServletResponse servletResponse = request.getNativeResponse(HttpServletResponse.class);
                   if (!servletResponse.isCommitted()) {
                      servletResponse.setContentType("text/plain;charset=utf-8");
                      servletResponse.getWriter().write("超时了");
                      servletResponse.getWriter().close();
                   }
                   return null;
                }
                
                可优化地方:
                       由于ThreadPoolTaskExecutor内部缓冲队列采用的是阻塞队列LinkedBlockingDeque,如果队列满了,外部线程会继续等待,需要设置HTTP请求的超时时间
                       线程池的大小配置
             
             
                  
       Spring MVC对异步处理的支持:                  ----  Servlet 3.0或以上的版本 + Spring 3.2以上版本
                                         maven:  最新的依赖
                                                <dependency>
                                                    <groupId>javax.servlet</groupId>
                                                    <artifactId>javax.servlet-api</artifactId>
                                                    <version>3.1.0</version>
                                                    <scope>provided</scope>
                                                </dependency>
                                               <dependency>
                                                   <groupId>org.springframework</groupId>
                                                   <artifactId>spring-webmvc</artifactId>
                                                   <version>4.3.2.RELEASE</version>
                                              </dependency>
            3种方式:
                 Callable<T>           ---  Spring MVC中线程池调用新线程完成处理,把最耗时的业务逻辑放到Callable
                                            对于Callable来说会默认使用SimpleAsyncTaskExecutor类来执行 
                 
                                       eg:  ---  这里有主线程和Callable子线程同时执行, 子线程运行完将结果返回, 比单线程(仅:主线程)要快
                                           @RequestMapping("/response-body")
                                      public @ResponseBody Callable<String> callable(final @RequestParam(required=false, defaultValue="true") boolean handled) {
                                           //进行一些与处理之后,把最耗时的业务逻辑部分放到Callable中,注意,如果你需要在new Callable中用到从页面传入的参数,需要在参数前加入final
                                          return new Callable<String>() {
                                              @Override
                                              public String call() throws Exception {
                                                  if(handled){
                                                      Thread.sleep(2000);
                                                  }else{
                                                      Thread.sleep(2000*2);
                                                  }
                                                  return "Callable result";
                                              }
                                          };
                                      }
                                      
                 WebAsyncTask
                                    1.在servlet中timeout是一个很重要的问题,servlet容器会尝试重用request和response对象,
                                      对于一个timeout但是实际上没有结束的异步请求来说,使用同一个request和response对象影响将无法估量 
                                      eg:
                                        使用WebAsyncTask指定超时时间                ---   如果Callable子线程在超时时间内未完成,则抛出自定义异常
                                        
                               @RequestMapping("/custom-timeout-handling")
                               public @ResponseBody WebAsyncTask<String> callableWithCustomTimeoutHandling() {
                                  System.out.println("Thread - name -" + Thread.currentThread().getName());
                                  Callable<String> callable = new Callable<String>() {
                                     @Override
                                     public String call() throws Exception {
                                        // Thread.sleep(5000);
                                        Thread.sleep(3000);
                                        System.out.println("Thread - name -" + Thread.currentThread().getName());
                                        return "Callable result";
                                     }
                                  };
                            
                                  System.out.println("Thread - name -" + Thread.currentThread().getName());
                                  // 这里设置Callable的超时时间为3s,超时则抛出超时异常,通过自定义的拦截器来处理该超时异常     
                                  // 对于Callable子线程的超时的处理,在springContext.xml中配置(这里:采取的处理是,抛出自定义的超时异常)
                                  // 即:如果在3s内,Callable子线程未完成,则抛出自定义的超时异常
                                  return new WebAsyncTask<String>(3000, callable);
                               }                                                                                                                                
                 
                 
                 DeferredResult<?>    ---  DeferredResult<?>  允许程序从一个线程中返回,而合适返回则由线程决定

    要知道什么是异步模式,就先要知道什么是同步模式,先看最典型的同步模式:

    (图1)

    浏览器发起请求,Web服务器开一个线程处理,处理完把处理结果返回浏览器。好像没什么好说的了,绝大多数Web服务器都如此般处理。现在想想如果处理的过程中需要调用后端的一个业务逻辑服务器,会是怎样呢?

    (图2)

    调就调吧,上图所示,请求处理线程会在Call了之后等待Return,自身处于阻塞状态。这也是绝大多数Web服务器的做法,一般来说这样做也够了,为啥?一来“长时间处理服务”调用通常不多,二来请求数其实也不多。要不是这样的话,这种模式会出现什么问题呢?——会出现的问题就是请求处理线程的短缺!因为请求处理线程的总数是有限的,如果类似的请求多了,所有的处理线程处于阻塞的状态,那新的请求也就无法处理了,也就所谓影响了服务器的吞吐能力。要更加好地发挥服务器的全部性能,就要使用异步,这也是标题上所说的“高性能的关键”。接下来我们来看看异步是怎么一回事:

     

    (图3)

    最大的不同在于请求处理线程对后台处理的调用使用了“invoke”的方式,就是说调了之后直接返回,而不等待,这样请求处理线程就“自由”了,它可以接着去处理别的请求,当后端处理完成后,会钩起一个回调处理线程来处理调用的结果,这个回调处理线程跟请求处理线程也许都是线程池中的某个线程,相互间可以完全没有关系,由这个回调处理线程向浏览器返回内容。这就是异步的过程。

    带来的改进是显而易见的,请求处理线程不需要阻塞了,它的能力得到了更充分的使用,带来了服务器吞吐能力的提升。

    Spring MVC的使用——DefferedResult

    要使用spring MVC的异步功能,你得先确保你用的是Servlet 3.0或以上的版本,Maven中如此配置:

    复制代码
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>4.2.3.RELEASE</version>
        </dependency>
    复制代码

    我这里使用的Servlet版本是3.1.0,Spring MVC版本是4.2.3,建议使用最新的版本。

    由于Spring MVC的良好封装,异步功能使用起来出奇的简单。传统的同步模式的Controller是返回ModelAndView,而异步模式则是返回DeferredResult<ModelAndView>。

    看这个例子:

    复制代码
    @RequestMapping(value="/asynctask", method = RequestMethod.GET)
    public DeferredResult<ModelAndView> asyncTask(){
        DeferredResult<ModelAndView> deferredResult = new DeferredResult<ModelAndView>();
        System.out.println("/asynctask 调用!thread id is : " + Thread.currentThread().getId());
        longTimeAsyncCallService.makeRemoteCallAndUnknownWhenFinish(new LongTermTaskCallback() {
            @Override
            public void callback(Object result) {
                System.out.println("异步调用执行完成, thread id is : " + Thread.currentThread().getId());
                ModelAndView mav = new ModelAndView("remotecalltask");
                mav.addObject("result", result);
                deferredResult.setResult(mav);
            }
        });
    }
    复制代码

    longTimeAsyncCallService是我写的一个模拟长时间异步调用的服务类,调用之,立即返回,当它处理完成时候,就钩起一个线程调用我们提供的回调函数,这跟“图3”描述的一样,它的代码如下:

    复制代码
    public interface LongTermTaskCallback {
        void callback(Object result);
    }
    
    public class LongTimeAsyncCallService {
        private final int CorePoolSize = 4;
        private final int NeedSeconds = 3;
        private Random random = new Random();
        private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(CorePoolSize);
        public void makeRemoteCallAndUnknownWhenFinish(LongTermTaskCallback callback){
            System.out.println("完成此任务需要 : " + NeedSeconds + " 秒");
            scheduler.schedule(new Runnable() {
                @Override
                public void run() {
                    callback.callback("长时间异步调用完成.");
                }
            }, "这是处理结果:)", TimeUnit.SECONDS);
        }
    }
    复制代码

    输出的结果是:

    /asynctask 调用!thread id is : 46
    完成此任务需要 : 3 秒
    异步调用执行完成, thread id is : 47

    由此可见返回结果的线程和请求处理线程不是同一线程。

    还有个叫WebAsyncTask

    返回DefferedResult<ModelAndView>并非唯一做法,还可以返回WebAsyncTask来实现“异步”,但略有不同,不同之处在于返回WebAsyncTask的话是不需要我们主动去调用Callback的,看例子:

    复制代码
    @RequestMapping(value="/longtimetask", method = RequestMethod.GET)
    public WebAsyncTask longTimeTask(){
        System.out.println("/longtimetask被调用 thread id is : " + Thread.currentThread().getId());
        Callable<ModelAndView> callable = new Callable<ModelAndView>() {
            public ModelAndView call() throws Exception {
                Thread.sleep(3000); //假设是一些长时间任务
                ModelAndView mav = new ModelAndView("longtimetask");
                mav.addObject("result", "执行成功");
                System.out.println("执行成功 thread id is : " + Thread.currentThread().getId());
                return mav;
            }
        };
        return new WebAsyncTask(callable);
    }
    复制代码

    其核心是一个Callable<ModelAndView>,事实上,直接返回Callable<ModelAndView>都是可以的,但我们这里包装了一层,以便做后面提到的“超时处理”。和前一个方案的差别在于这个Callable的call方法并不是我们直接调用的,而是在longTimeTask返回后,由Spring MVC用一个工作线程来调用,执行,打印出来的结果:

    /longtimetask被调用 thread id is : 56
    执行成功 thread id is : 57

    可见确实由不同线程执行的,但这个WebAsyncTask可不太符合“图3”所描述的技术规格,它仅仅是简单地把请求处理线程的任务转交给另一工作线程而已。

    处理超时

    如果“长时间处理任务”一直没返回,那我们也不应该让客户端无限等下去啊,总归要弄个“超时”出来。如图:

     

    (图4)

    其实“超时处理线程”和“回调处理线程”可能都是线程池中的某个线程,我为了清晰点把它们分开画而已。增加这个超时处理在Spring MVC中非常简单,先拿WebAsyncTask那段代码来改一下:

    复制代码
    @RequestMapping(value="/longtimetask", method = RequestMethod.GET)
    public WebAsyncTask longTimeTask(){
        System.out.println("/longtimetask被调用 thread id is : " + Thread.currentThread().getId());
        Callable<ModelAndView> callable = new Callable<ModelAndView>() {
            public ModelAndView call() throws Exception {
                Thread.sleep(3000); //假设是一些长时间任务
                ModelAndView mav = new ModelAndView("longtimetask");
                mav.addObject("result", "执行成功");
                System.out.println("执行成功 thread id is : " + Thread.currentThread().getId());
                return mav;
            }
        };
        
        
        WebAsyncTask asyncTask = new WebAsyncTask(2000, callable);
        asyncTask.onTimeout(
                new Callable<ModelAndView>() {
                    public ModelAndView call() throws Exception {
                        ModelAndView mav = new ModelAndView("longtimetask");
                        mav.addObject("result", "执行超时");
                        System.out.println("执行超时 thread id is :" + Thread.currentThread().getId());
                        return mav;
                    }
                }
        );
        return new WebAsyncTask(3000, callable);
    }
    复制代码

    注意看红色字体部分代码,这就是前面提到的为什么Callable还要外包一层的缘故,给WebAsyncTask设置一个超时回调,即可实现超时处理,在这个例子中,正常处理需要3秒钟,而超时设置为2秒,所以肯定会出现超时,执行打印log如下:

    /longtimetask被调用 thread id is : 59
    执行超时 thread id is :61
    执行成功 thread id is : 80

    嗯?明明超时了,怎么还会“执行成功”呢?超时归超时,超时并不会打断正常执行流程,但注意,出现超时后我们给客户端返回了“超时”的结果,那接下来即便正常处理流程成功,客户端也收不到正常处理成功所产生的结果了,这带来的问题就是:客户端看到了“超时”,实际上操作到底有没有成功,客户端并不知道,但通常这也不是什么大问题,因为用户在浏览器上再刷新一下就好了。:D

    好,再来看DefferedResult方式的超时处理:

    复制代码
        @RequestMapping(value = "/asynctask", method = RequestMethod.GET)
        public DeferredResult<ModelAndView> asyncTask() {
            DeferredResult<ModelAndView> deferredResult = new DeferredResult<ModelAndView>(2000L);
            System.out.println("/asynctask 调用!thread id is : " + Thread.currentThread().getId());
            longTimeAsyncCallService.makeRemoteCallAndUnknownWhenFinish(new LongTermTaskCallback() {
                @Override
                public void callback(Object result) {
                    System.out.println("异步调用执行完成, thread id is : " + Thread.currentThread().getId());
                    ModelAndView mav = new ModelAndView("remotecalltask");
                    mav.addObject("result", result);
                    deferredResult.setResult(mav);
                }
            });
    
            deferredResult.onTimeout(new Runnable() {
                @Override
                public void run() {
                    System.out.println("异步调用执行超时!thread id is : " + Thread.currentThread().getId());
                    ModelAndView mav = new ModelAndView("remotecalltask");
                    mav.addObject("result", "异步调用执行超时");
                    deferredResult.setResult(mav);
                }
            });
    
            return deferredResult;
        }
    复制代码

    非常类似,对吧,我把超时设置为2秒,而正常处理需要3秒,一定会超时,执行结果如下:

    /asynctask 调用!thread id is : 48
    完成此任务需要 : 3 秒
    异步调用执行超时!thread id is : 51
    异步调用执行完成, thread id is : 49

    完全在我们预料之中。

    异常处理

    貌似没什么差别,在Controller中的处理和之前同步模式的处理是一样一样的:

        @ExceptionHandler(Exception.class)
        public ModelAndView handleAllException(Exception ex) {
            ModelAndView model = new ModelAndView("error");
            model.addObject("result", ex.getMessage());
            return model;
        }

    还要再弄个全局的异常处理啥的,和过去的做法都一样,在此不表了。

    Spring MVC 处理异步请求

    参考:
  • 相关阅读:
    Ubuntu adb devices :???????????? no permissions (verify udev rules) 解决方法
    ubuntu 关闭显示器的命令
    ubuntu android studio kvm
    ubuntu 14.04版本更改文件夹背景色为草绿色
    ubuntu 创建桌面快捷方式
    Ubuntu 如何更改用户密码
    ubuntu 14.04 返回到经典桌面方法
    ubuntu 信使(iptux) 创建桌面快捷方式
    Eclipse failed to get the required ADT version number from the sdk
    Eclipse '<>' operator is not allowed for source level below 1.7
  • 原文地址:https://www.cnblogs.com/shihaiming/p/6428323.html
Copyright © 2011-2022 走看看