异步,再教你一种高并发大杀器(干货)

837 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

写在前面

今天我们来聊聊老生常谈的异步、并发问题,异步是解决并发的最有效的方法。解决耗时让问题,让查询数据库、rpc远程调用等各种耗时操作能够同时进行,达到让接口快速响应的目的,进而有效解决并发问题。

好,话不多说,我们进入今天的正题,不知道有同学用过没 DeferredResult

今天我就跟大家来聊聊如果利用 DeferredResult 实现异步编程,欢迎大家看下我以前写过的一篇

干货!SpringBoot利用监听事件,实现异步操作

Servlet

讲DeferredResult之前。我们先聊聊 Servlet,这个大家都不陌生,项目中不论你使用struts2,还是使用的springmvc,本质上都是封装的servlet。

在servlet3.0之后支持了异步化,我们采用异步化代码如下:


//测试servlet 异步化
//asyncSupported =true 开启异步

@WebServlet(urlPatterns = "/testSyncServlet",asyncSupported =true)
public class WorkServlet extends HttpServlet{
    private static final long serialVersionUID = 1L;
    
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }
 
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置ContentType,关闭缓存
        resp.setContentType("text/plain;charset=UTF-8");
        resp.setHeader("Cache-Control","private");
        resp.setHeader("Pragma","no-cache");
        final PrintWriter writer= resp.getWriter();
        writer.println("servlet 异步化");
        writer.flush();
        List<String> textArr=new ArrayList<String>();
        for (int i = 0; i < 10; i++) {
            textArr.add("zuoye"+i);;
        }
        
        //开启异步请求
        final AsyncContext ac=req.startAsync();
        doProcess(ac, textArr);
        writer.println("servlet 异步化完成");
        writer.flush();
    }
 
    private void doProcess(final AsyncContext ac, final List<String> textArr) {
        ac.setTimeout(1*60*60*1000L);
        ac.start(new Runnable() {
            @Override
            public void run() {
                //通过response获得字符输出流
                try {
                    PrintWriter writer=ac.getResponse().getWriter();
                    for (String text:textArr) {
                        writer.println("\""+text+"\"请求处理中");
                        Thread.sleep(1*1000L);
                        writer.flush();
                    }
                    ac.complete();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

上面最重要的就是 AsyncContext ac=req.startAsync(); 开启异步请求。setTimeout设置超时时间,ac.start 开启线程执行业务请求。

Spring MVC 的异步处理能力

我们都知道,Spring MVC围绕前端控制器模式(Front ControllerPattern)设计,其中中央Servlet DispatcherServlet为请求处理提供共享的路由算法,负责对请求进行路由分派。

DispatcherServlet与任何Servlet一样,需要使用Java配置或webxml根据Servlet规范进行声明和映射。反过来,DispatcherServlet使用Spring配置来发现请求映射、视图解析、异常处理等所需的委托组件。

那么肯定的是 Spring MVC与前面讲解的Servlet 3.0异步请求处理有很深的集成:

  • DeferredResult和Callable作为controller方法中的返回值,并为单个异步返回值提供基本支持。

  • controller可以使用反应式客户端并返回反应式类型,以进行反应式处理。

Spring MVC内部通过调用request.startAsync()将ServletRequest置于异步模式

好,之所以讲这些是为了让大家从头部明白 DeferredResult 是如何与springMvc结合 大家用的都是springboot来跑项目的,所有肯定不会让你只看原生Servlet的。下面介绍我们今天的主角

基于DeferredResult的异步处理

上面提到,一旦在Servlet容器中启用了异步请求处理功能,controller方法就可以使用DeferredResult包装任何支持的方法返回值。

1、创建 DeferredResult 实例

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

2、设置超时、完成后操作

deferredResult.onTimeout(() -> {
    log.info("调用超时");
});

deferredResult.onCompletion(() -> {
    log.info("调用完成");
});

3、 开启线程设置结果

new Thread(() > {    
   try {     
      
      //模拟耗时操作
      TimeUnit.SECONDS.sleep(10);   
      //返回OK结果
      deferredResult.setResult("ok"catch (InterruptedException e) {        
      e.printStackTrace();    
   }
}).start();

完整代码

//先定义线程池
private static ThreadPoolExecutor BIZ_POOL = new ThreadPoolExecutor(8,8,1, TimeUnit.SECONDS,new LinkedBlockingQueue<>(1),
        new ThreadPoolExecutor.CallerRunsPolicy());
        
 //调用DeferredResult异步机制
@PostMapping("/testSync")
DeferredResult<String> listPostDeferredResult(){
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    BIZ_POOL.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
                deferredResult.setResult("ok");
            }catch (Exception e){
                e.printStackTrace();
                deferredResult.setErrorResult("error");
            }
        }
    });

    return deferredResult;
}

总结

上述代码我们创建了一个业务线程池BIZ_POOL,然后controller方法在listPostDeferredResult内创建了一个DeferredResult对象,接着向业务线程池BIZ_POOL提交我们的请求处理逻辑(其内部处理完毕后把结果设置到创建的DeferredResult),最后返回创建的DeferredResult对象。其整个处理过程如下:

  • Tomcat容器接收路径为personDeferredResult的请求后,会分配一个容器线程来执行DispatcherServlet进行请求分派,请求被分到含有personDeferredResult路径的controller,然后执行listPostDeferredResult方法,该方法内创建了一个DeferredResult对象,然后把处理任务提交到了线程池进行处理,最后返回DeferredResult对象。

  • Spring MVC内部在personDeferredResult方法返回后会保存DeferredResult对象到内存队列或者列表,然后会调用request.startAsync()开启异步处理,并且调用DeferredResult对象的setResultHandler方法,设置当异步结果产生后对结果进行重新路由的回调函数(逻辑在WebAsyncManager的startDeferredResultProcessing方法)

  • 最终在业务线程池中执行的异步任务会产生一个结果,该结果会被设置到DeferredResult对象,然后设置的回调函数会被调用,接着Spring MVC会分派请求结果回到Servlet容器继续完成处理,DispatcherServlet被再次调用,使用返回的异步结果继续进行处理,最终把响应结果写回请求方。

OK 今天关于DeferredResult的讲解就到这里 由于篇幅有限 具体的源码和原理我打算放到后面具体讲下,感兴趣的关注我。后面我们共同学习

与Callable区别

DeferredResult与Callable实现的功能类似,都是异步返回,只不过Callable不能直接设置超时时间,需要与FutureTask配合才行;DeferredResult可直接设置超时时间。

我以前写过一篇关于Callable 的文章 大家感兴趣的可以看下:

干货!并发编程——Future

弦外之音

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

聊聊不一样的策略模式(值得收藏)

copy对象,这个操作有点骚!