欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?

程序员文章站 2023-02-17 20:07:22
前言 博文地址:https://sourl.cn/URptix 当一个 HTTP 请求到达 Tomcat,Tomcat 将会从线程池中取出线程,然后按照如下流程处理请求: 将请求信息解析为 分发到具体 Servlet 处理相应的业务 通过 将响应结果返回给等待客户端 整体流程如下所示: 这是我们日常 ......

前言

博文地址:https://sourl.cn/urptix

当一个 http 请求到达 tomcat,tomcat 将会从线程池中取出线程,然后按照如下流程处理请求:

  • 将请求信息解析为 httpservletrequest
  • 分发到具体 servlet 处理相应的业务
  • 通过 httpservletresponse 将响应结果返回给等待客户端

整体流程如下所示:

大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?

这是我们日常最常用同步请求模型,所有动作都交给同一个 tomcat 线程处理,所有动作处理完成,线程才会被释放回线程池。

想象一下如果业务需要较长时间处理,那么这个 tomcat 线程其实一直在被占用,随着请求越来越多,可用 i/o 线程越来越少,直到被耗尽。这时后续请求只能等待空闲 tomcat 线程,这将会加长了请求执行时间。

如果客户端不关心返回业务结果,这时我们可以自定义线程池,将请求任务提交给线程池,然后立刻返回。

也可以使用 spring async 任务,大家感兴趣可以自行查找一下资料

但是很多场景下,客户端需要处理返回结果,我们没办法使用上面的方案。在 servlet2 时代,我们没办法优化上面的方案。

不过等到 servlet3 ,引入异步 servelt 新特性,可以完美解决上面的需求。

异步 servelt 执行请求流程:

  • 将请求信息解析为 httpservletrequest
  • 分发到具体 servlet 处理,将业务提交给自定义业务线程池,请求立刻返回,tomcat 线程立刻被释放
  • 当业务线程将任务执行结束,将会将结果转交给 tomcat 线程
  • 通过 httpservletresponse 将响应结果返回给等待客户端

引入异步 servelt3 整体流程如下:

大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?

使用异步 servelt,tomcat 线程仅仅处理请求解析动作,所有耗时较长的业务操作全部交给业务线程池,所以相比同步请求, tomcat 线程可以处理 更对请求。

虽然我们将业务处理交给业务线程池异步处理,但是对于客户端来讲,其还在同步等待响应结果

可能有些同学会觉得异步请求将会获得更快响应时间,其实不是的,相反可能由于引入了更多线程,增加线程上下文切换时间。

虽然没有降低响应时间,但是通过请求异步化带来其他明显优点

  • 可以处理更高并发连接数,提高系统整体吞吐量
  • 请求解析与业务处理完全分离,职责单一
  • 自定义业务线程池,我们可以更容易对其监控,降级等处理
  • 可以根据不同业务,自定义不同线程池,相互隔离,不用互相影响

所以具体使用过程,我们还需要进行的相应的压测,观察响应时间以及吞吐量等其他指标,综合选择。

异步 servelt 使用方式

异步 servelt 使用方式不是很难,小黑哥总结就是就是下面三板斧:

  1. httpservletrequest#startasync 获取 asynccontext 异步上下文对象
  2. 使用自定义的业务线程池处理业务逻辑
  3. 业务线程处理结束,通过 asynccontext#complete 返回响应结果

下面的例子将会使用 springboot ,web 容器选择 tomcat

示例代码如下:

executorservice executorservice = executors.newfixedthreadpool(10);

@requestmapping("/hello")
public void hello(httpservletrequest request) {
    asynccontext asynccontext = request.startasync();
    // 超时时间
    asynccontext.settimeout(10000);
    executorservice.submit(() -> {
        try {
            // 休眠 5s,模拟业务操作
            timeunit.seconds.sleep(5);
            // 输出响应结果
            asynccontext.getresponse().getwriter().println("hello world");
            log.info("异步线程处理结束");
        } catch (exception e) {
            e.printstacktrace();
        } finally {
            asynccontext.complete();
        }
    });
    log.info("servlet 线程处理结束");
}

浏览器访问该请求将会同步等待 5s 得到输出响应,应用日志输出结果如下:

2020-03-24 07:27:08.997  info 79257 --- [nio-8087-exec-4] com.xxxx   : servlet 线程处理结束
2020-03-24 07:27:13.998  info 79257 --- [pool-1-thread-3] com.xxxx   : 异步线程处理结束

这里我们需要注意设置合理的超时时间,防止客户端长时间等待。

springmvc

servlet3 api ,无法使用 springmvc 为我们提供的特性,我们需要自己处理响应信息,处理方式相对繁琐。

springmvc 3.2 基于 servelt3 引入异步请求处理方式,我们可以跟使用同步请求一样,方便使用异步请求。

springmvc 提供有两种异步方式,只要将 controller 方法返回值修改下述类即可:

  • deferredresult
  • callable

deferredresult

deferredresult 是 springmvc 3.2 之后引入新的类,只要让请求方法返回 deferredresult,就可以快速使用异步请求,示例代码如下:

executorservice executorservice = executors.newfixedthreadpool(10);

@requestmapping("/hello_v1")
public deferredresult<string> hello_v1() {
    // 设置超时时间
    deferredresult<string> deferredresult = new deferredresult<>(7000l);
    // 异步线程处理结束,将会执行该回调方法
    deferredresult.oncompletion(() -> {
        log.info("异步线程处理结束");
    });
    // 如果异步线程执行时间超过设置超时时间,将会执行该回调方法
    deferredresult.ontimeout(() -> {
        log.info("异步线程超时");
        // 设置返回结果
        deferredresult.seterrorresult("timeout error");
    });
    deferredresult.onerror(throwable -> {
        log.error("异常", throwable);
        // 设置返回结果
        deferredresult.seterrorresult("other error");
    });
    executorservice.submit(() -> {
        try {
            timeunit.seconds.sleep(5);
            deferredresult.setresult("hello_v1");
            // 设置返回结果
        } catch (exception e) {
            e.printstacktrace();
            // 若异步方法内部异常
            deferredresult.seterrorresult("error");
        }
    });
    log.info("servlet 线程处理结束");
    return deferredresult;

}

创建 deferredresult 实例时可以传入特定超时时间。另外我们可以设置默认超时时间:

# 异步请求超时时间
spring.mvc.async.request-timeout=2000

如果异步程序执行完成,可以调用 deferredresult#setresult返回响应结果。此时若有设置 deferredresult#oncompletion 回调方法,将会触发该回调方法。

go to implementation(s)

最后 deferredresult 还提供其他异常的回调方法 onerror,起初小黑哥以为只要异步线程内发生异常,就会触发该回调方法。尝试在异步线程内抛出异常,但是无法成功触发。

后续小黑哥查看这个方法的 doc,当 web 容器线程处理异步请求是时发生异常,才能成功触发。

大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?

小黑哥不知道如何才能发生这个异常,有经验的小伙伴们的可以留言告知下。

callable

spring 另外还提供一种异步请求使用方式,直接使用 jdk callable。示例代码如下:

@requestmapping("/hello_v2")
public callable<string> hello_v2() {
    return new callable<string>() {
        @override
        public string call() throws exception {
            timeunit.seconds.sleep(5);
            log.info("异步方法结束");
            return "hello_v2";
        }
    };
}

默认情况下,直接执行将会输出 warn 日志:

大部分程序员还不知道的 Servelt3 异步请求,原来这么简单?

这是因为默认情况使用 simpleasynctaskexecutor 执行异步请求,每次调用执行都将会新建线程。由于这种方式不复用线程,生产不推荐使用这种方式,所以我们需要使用线程池代替。

我们可以使用如下方式自定义线程池:

@bean(taskexecutionautoconfiguration.application_task_executor_bean_name)
public asynctaskexecutor executor() {
    threadpooltaskexecutor threadpooltaskexecutor = new threadpooltaskexecutor();
    threadpooltaskexecutor.setthreadnameprefix("test-");
    threadpooltaskexecutor.setcorepoolsize(10);
    threadpooltaskexecutor.setmaxpoolsize(20);
    return threadpooltaskexecutor;
}

注意 bean 名称一定要是 applicationtaskexecutor,若不一致, spring 将不会使用自定义线程池。

或者可以直接使用 springboot 配置文件方式配置代替:

# 核心线程数
spring.task.execution.pool.core-size=10
# 最大线程数
spring.task.execution.pool.max-size=20
# 线程名前缀
spring.task.execution.thread-name-prefix=test
# 还有另外一些配置,读者们可以自行配置

这种方式异步请求的超时时间只能通过配置文件方式配置。

spring.mvc.async.request-timeout=10000

如果需要为单独请求的配置特定的超时时间,我们需要使用 webasynctask 包装 callable

@requestmapping("/hello_v3")
public webasynctask<string> hello_v3() {
    system.out.println("asdas");
    callable<string> callable=new callable<string>() {
        @override
        public string call() throws exception {
            timeunit.seconds.sleep(5);
            log.info("异步方法结束");
            return "hello_v3";
        }
    };
    // 单位 ms
    webasynctask<string> webasynctask=new webasynctask<>(10000,callable);
    return webasynctask;
}

总结

springmvc 两种异步请求方式,本质上就是帮我们包装 servlet3 api ,让我们不用关心具体实现细节。虽然日常使用我们一般会选择使用 springmvc 两种异步请求方式,但是我们还是需要了解异步请求实际原理。所以大家如果在使用之前,可以先尝试使用 servlet3 api 练习,后续再使用 springmvc。

reference

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客: