异步处理

    实现Servlet容器的时候,只要每处理一个请求,就创建一个新线程处理它,就能保证正确实现了Servlet线程模型。在实际产品中,例如Tomcat,总是通过线程池来处理请求,它仍然符合一个请求从头到尾都由某一个线程处理。

    这种线程模型非常重要,因为Spring的JDBC事务是基于实现的,如果在处理过程中,一会由线程A处理,一会又由线程B处理,那事务就全乱套了。此外,很多安全认证,也是基于ThreadLocal实现的,可以保证在处理请求的过程中,各个线程互不影响。

    但是,如果一个请求处理的时间较长,例如几秒钟甚至更长,那么,这种基于线程池的同步模型很快就会把所有线程耗尽,导致服务器无法响应新的请求。如果把长时间处理的请求改为异步处理,那么线程池的利用率就会大大提高。Servlet从3.0规范开始添加了异步支持,允许对一个请求进行异步处理。

    我们先来看看在Spring MVC中如何实现对请求进行异步处理的逻辑。首先建立一个Web工程,然后编辑web.xml文件如下:

    和前面普通的MVC程序相比,这个web.xml主要有几点不同:

    • 不能再使用<!DOCTYPE ...web-app_2_3.dtd">的DTD声明,必须用新的支持Servlet 3.1规范的XSD声明,照抄即可;
    • DispatcherServlet的配置多了一个<async-supported>,默认值是false,必须明确写成true,这样Servlet容器才会支持async处理。

    下一步就是在Controller中编写async处理逻辑。我们以ApiController为例,演示如何异步处理请求。

    第一种async处理方式是返回一个Callable,Spring MVC自动把返回的Callable放入线程池执行,等待结果返回后再写入响应:

    1. @GetMapping("/users")
    2. public Callable<List<User>> users() {
    3. return () -> {
    4. // 模拟3秒耗时:
    5. try {
    6. Thread.sleep(3000);
    7. } catch (InterruptedException e) {
    8. }
    9. return userService.getUsers();
    10. }

    第二种async处理方式是返回一个DeferredResult对象,然后在另一个线程中,设置此对象的值并写入响应:

    运行程序,当我们访问时,假定用户存在,则浏览器在1秒后返回结果:

    访问一个不存在的User ID,则等待1秒后返回错误结果:

    deferred-result-error

    当我们使用async模式处理请求时,原有的Filter也可以工作,但我们必须在web.xml中添加<async-supported>并设置为true。我们用两个Filter:SyncFilter和AsyncFilter分别测试:

    1. <web-app ...>
    2. ...
    3. <filter>
    4. <filter-name>sync-filter</filter-name>
    5. <filter-class>com.itranswarp.learnjava.web.SyncFilter</filter-class>
    6. </filter>
    7. <filter>
    8. <filter-name>async-filter</filter-name>
    9. <filter-class>com.itranswarp.learnjava.web.AsyncFilter</filter-class>
    10. <async-supported>true</async-supported>
    11. </filter>
    12. <filter-mapping>
    13. <url-pattern>/api/version</url-pattern>
    14. </filter-mapping>
    15. <filter-mapping>
    16. <filter-name>async-filter</filter-name>
    17. <url-pattern>/api/*</url-pattern>
    18. </filter-mapping>
    19. ...

    一个声明为支持<async-supported>的Filter既可以过滤async处理请求,也可以过滤正常的同步处理请求,而未声明<async-supported>的Filter无法支持async请求,如果一个普通的Filter遇到async请求时,会直接报错,因此,务必注意普通Filter的<url-pattern>不要匹配async请求路径。

    logback.xml配置文件中,我们把输出格式加上[%thread],可以输出当前线程的名称:

    对于同步请求,例如/api/version,我们可以看到如下输出:

    1. 2020-05-16 11:22:40 [http-nio-8080-exec-1] INFO c.i.learnjava.web.SyncFilter - start SyncFilter...
    2. 2020-05-16 11:22:40 [http-nio-8080-exec-1] INFO c.i.learnjava.web.AsyncFilter - start AsyncFilter...
    3. 2020-05-16 11:22:40 [http-nio-8080-exec-1] INFO c.i.learnjava.web.ApiController - get version...
    4. 2020-05-16 11:22:40 [http-nio-8080-exec-1] INFO c.i.learnjava.web.AsyncFilter - end AsyncFilter.
    5. 2020-05-16 11:22:40 [http-nio-8080-exec-1] INFO c.i.learnjava.web.SyncFilter - end SyncFilter.

    对于异步请求,例如/api/users,我们可以看到如下输出:

    可见,AsyncFilterApiController是由同一个线程执行的,但是,返回响应的是另一个线程。

    DeferredResult测试,可以看到如下输出:

    1. 2020-05-16 11:25:24 [http-nio-8080-exec-8] INFO c.i.learnjava.web.AsyncFilter - start AsyncFilter...
    2. 2020-05-16 11:25:25 [Thread-2] INFO c.i.learnjava.web.ApiController - deferred result is set.

    同样,返回响应的是另一个线程。

    在实际使用时,经常用到的就是DeferredResult,因为返回DeferredResult时,可以设置超时、正常结果和错误结果,易于编写比较灵活的逻辑。

    使用async异步处理响应时,要时刻牢记,在另一个异步线程中的事务和Controller方法中执行的事务不是同一个事务,在Controller中绑定的ThreadLocal信息也无法在异步线程中获取。

    此外,Servlet 3.0规范添加的异步支持是针对同步模型打了一个“补丁”,虽然可以异步处理请求,但高并发异步请求时,它的处理效率并不高,因为这种异步模型并没有用到真正的“原生”异步。Java标准库提供了封装操作系统的异步IO包java.nio,是真正的多路复用IO模型,可以用少量线程支持大量并发。使用NIO编程复杂度比同步IO高很多,因此我们很少直接使用NIO。相反,大部分需要高性能异步IO的应用程序会选择这样的框架,它基于NIO提供了更易于使用的API,方便开发异步应用程序。

    下载练习: (推荐使用IDE练习插件快速下载)

    异步处理 - 图2