浅聊接口性能优化——异步处理

HTTP作为一种无状态的协议采用的是请求-应答的模式,每当客户端发起的请求到达服务器,Servlet 容器通常会为每个请求使用一个线程来处理。为了避免线程创建和销毁的资源消耗,一般会采用线程池,而线程池中的线程数量是有限的,当线程池中的线程被全部使用,客户端只能等待有空闲线程处理请求。

在这里插入图片描述

实际场景中,部分线程可能因为等待数据库查询结果或远程 Web 资源被阻塞,如果阻塞时间过长,线程池中的线程很快就被耗尽,从而导致无法处理其他请求。

Servlet 异步处理

为了提高系统的吞吐量,我们需要尽量使处理请求的线程处于非空闲状态。如果能够将那些长时间阻塞的线程利用起来处理新请求,由其他线程等资源满足时再继续处理前面的请求,这样对吞吐量的提升就会有很大的帮助。

Java EE 自 Servlet 3.0 开始对 Servlet 和 Filter 提供了异步支持,如果 Servlet 和 Filter 在处理请求时可能会发生阻塞,可以将阻塞请求线程的操作分配到异步线程,然后将处理请求的线程归还到 Servlet 容器中的线程池,而不产生响应,当异步线程中的操作完成,异步线程可以直接产生响应或将请求重新分派到容器中的 Servlet 处理。

在这里插入图片描述

Servlet 异步处理实战

先通过一个案例了解如何使用 Servlet 中的异步处理。

默认情况下 Servlet 和 Filter 都不支持异步,需要在部署描述符或注解中开启异步支持。

部署描述符开启异步支持示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    <servlet>
        <servlet-name>asyncA</servlet-name>
        <servlet-class>com.zzuhkp.mvc.AsyncServlet</servlet-class>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>asyncA</servlet-name>
        <url-pattern>/async/a</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>asyncFilter</filter-name>
        <filter-class>com.zzuhkp.mvc.AsyncFilter</filter-class>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>asyncFilter</filter-name>
        <servlet-name>asyncA</servlet-name>
    </filter-mapping>
</web-app>

部署描述符开启异步支持的重点是设置 servlet 或 filter 标签下的 async-supported 值为 true。

注解开启异步支持的示例如下:

@WebFilter(value = "/async/a", asyncSupported = true)
public class AsyncFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }
}

@WebServlet(urlPatterns = "/async/a", asyncSupported = true)
public class AsyncServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 开启异步处理
        AsyncContext asyncContext = req.startAsync(req, resp);
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 2. 使用新线程执行耗时操作
                    Thread.sleep(10000L);
                    // 3. 耗时操作完成后进行响应
                    asyncContext.getResponse().getWriter().write("this is a async servlet");
                    // 4. 通知容器异步操作完成
                    asyncContext.complete();
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

通过注解开启异步支持的重点是设置 @WebFilter 或 @WebServlet 中的 asyncSupported 为 true。

注意上述 Servlet 还列出了进行异步操作的常用步骤:

  1. 先使用 ServletRequest#startAsync(ServletRequest, ServletResponse) 开启异步。
  2. 开启异步后使用新线程进行异步处理,执行耗时操作。
  3. 新线程耗时操作完成后可以使用取到的资源信息发起响应。
  4. 最后调用第一步开启异步支持返回的异步上下文 AsyncContext#complete 方法通知容器异步处理已经结束。

Servlet 异步处理详解

开启异步支持

开启异步支持有两个方法,分别如下:

  • ServletRequest#startAsync(ServletRequest,ServletResponse)
  • ServletRequest#startAsync()

这两个参数都将返回一个异步处理的上下文 AsyncContext,不同的是如果使用了无参的 #startAsync 方法,AsyncContext 内部持有的 request、response 将是原始的,无论 Filter 是否对 request、response 进行了包装。

结束异步处理

异步处理完成后有两种结束的方式:一种如上面的示例通知容器返回响应到客户端,另一种是通知容器使用其他 Servlet 继续处理请求。

关联的方法有4个:

  • AsyncContext#complete
  • AsyncContext#dispatch()
  • AsyncContext#dispatch(String)
  • AsyncContext#dispatch(ServletContext, String)

AsyncContext 中的 #complete 用于在异步线程中通知容器向客户端发出响应,此后异步线程不可再产生响应。

AsyncContext 中的 #dispatch 用于通知容器重新派发请求。无参数的重载方法重新派发请求到当前请求路径,有参数的重载方法可以指定派发请求的路径。

派发类型判断

由于异步处理后可以重新派发请求到当前 URL,因此需要判断派发类型,知道当前请求是从哪里产生的,从而使用不同处理逻辑,这可以通过 ServletRequest#getDispatcherType 方法来实现,这个方法返回的是一个 DispatcherType 枚举类型,每个枚举值的含义如下:

public enum DispatcherType {
    // request.getRequestDispatcher("/path").forward(request,response) 产生的请求
    FORWARD,
    // request.getRequestDispatcher("/path").include(request,response) 产生的请求
    INCLUDE,
    // 客户端正常发起请求
    REQUEST,
    // 异步处理 AsyncContext#dispatch 分派的请求
    ASYNC,
    // Servlet 产生错误,转发请求到错误页面
    ERROR
}

异步处理监听

异步处理开始和结束之间,容器还会产生一些事件,可以通过 AsyncContext#addListener(AsyncListener) 方法添加对异步事件的监听,具体可以监听的事件如下:

public interface AsyncListener extends EventListener {
    // 异步处理完成
    public void onComplete(AsyncEvent event) throws IOException;
    // 异步处理超时
    public void onTimeout(AsyncEvent event) throws IOException;
    // 异步处理发生异常
    public void onError(AsyncEvent event) throws IOException;
    // ServletRequest#startAsync 重新开启异步
    public void onStartAsync(AsyncEvent event) throws IOException;     
}

异步处理默认的超时时间是 30 秒,可以通过 AsyncContext#setTimeout 设置超时时间,以设置时间重新计算。

Spring MVC 异步处理

Spring MVC 结合自身特性,对 Servlet 中的异步处理进行了封装,使异步处理更为简便。

快速体验 Spring MVC 异步处理

Spring MVC 手动配置 DispatcherServlet 需要指定 async-supported 为 true,Spring Boot 环境下已经默认开启了异步处理的支持。

在 Spring MVC 中使用异步处理最简单的方式是在 controller 方法中直接返回 Callable 类型,示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public Callable<String> test() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "this is a test";
            }
        };
        return callable;
    }
}

controller 方法返回 Callable 类型之后,Spring 会自动使用异步线程池调用 Callable#call 方法,然后对 #call 方法返回值重新解析,解析方式和普通的 controller 方法一致,上述示例代码将向浏览器输出一段文字。

Spring MVC 异步处理常用的两种方式

Callable

Callable 作为 controller 方法返回值是最常用的一种方式,这种方式会使用 Spring 默认的线程池进行异步处理。具体可以参见上面的示例。

DeferredResult

如果需要指定异步处理的线程池,将 DeferredResult 作为 controller 方法的返回值是更好的选择,DeferredResult 不仅可以手动指定线程池,还可以配置异步处理的回调,如超时、完成、错误。示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public DeferredResult<String> test() {
        DeferredResult<String> deferredResult = new DeferredResult<>();
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 设置异步处理结果
                deferredResult.setResult("this is a test");
            }
        });
        // 设置异步处理回调
        deferredResult.onTimeout(() -> System.out.println("异步处理超时"));
        deferredResult.onCompletion(() -> System.out.println("异步处理完成"));
        deferredResult.onError((throwable) -> System.out.println("异步处理错误:" + throwable.getMessage()));

        return deferredResult;
    }
}

上述代码将 DeferredResult 作为 controller 返回值,然后在线程池中手动设置了返回的结果,相对来说更为灵活。

Spring MVC 异步处理的其他方式

除了上述 Callable 和 DeferredResult 两种类型作为 controller 方法返回值,还有其他几种使用相对没那么频繁的类型可以作为 controller 方法的返回值类型,这几种类型与 Callable 或 DeferredResult 相互适配。

StreamingResponseBody、ResponseEntity<StreamingResponseBody>

StreamingResponseBody 可以使用原始的方式输出响应,Spring 内部将这个类适配为 Callable,在异步处理的时候回调这个接口然后输出响应。

ResponseEntity<StreamingResponseBody> 与 StreamingResponseBody 在 Spring 内部处理处理方式相似,Spring 会先根据 ResponseEntity 设置 HTTP 响应码、响应头,然后解析出 StreamingResponseBody 处理。

StreamingResponseBody 示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public StreamingResponseBody test() {
        StreamingResponseBody body = new StreamingResponseBody() {
            @Override
            public void writeTo(OutputStream outputStream) throws IOException {
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
                writer.write("this is a test");
            }
        };
        return body;
    }
}

WebAsyncTask

WebAsyncTask 是 Callable 最底层的实现,Callable 最终将适配为 WebAsyncTask,这个类和 DeferredResult 功能类似,可以指定异步执行线程池、异步执行回调,由于底层使用了 Callable ,因此不能手动指定何时产生响应。示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public WebAsyncTask<String> test() {
        // 设置超时时间、线程池、异步任务
        WebAsyncTask<String> task = new WebAsyncTask<>(5000L, new SimpleAsyncTaskExecutor(), new Callable<String>() {
            @Override
            public String call() throws Exception {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 返回异步处理结果
                return "this ia a test";
            }
        });

        // 设置异步处理回调
        task.onTimeout(() -> "异步处理超时");
        task.onCompletion(() -> System.out.println("异步处理完成"));
        task.onError(() -> "异步处理错误");

        return task;
    }
}

ListenableFuture

ListenableFuture 是 Spring 对 Future 扩展提出的接口,可以在任务执行成功或者失败时回调给定的接口方法。在异步处理中,如果 controller 方法返回这个类型,Spring 会将其适配为 DeferredResult,异步任务执行成功后设置异步处理的结果。从功能上来说弱于 DeferredResult,不能设置超时时间及超时回调。 示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public ListenableFuture<String> test() {
        ListenableFutureTask<String> task = new ListenableFutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                // 模拟耗时的操作
                Thread.sleep(5000L);
                // 返回异步处理结果
                return "this is a test";
            }
        });
        task.addCallback(new ListenableFutureCallback<String>() {
            @Override
            public void onFailure(Throwable ex) {
                System.out.println("异步任务异常:" + ex.getMessage());
            }

            @Override
            public void onSuccess(String result) {
                System.out.println("异步任务执行完成");
            }
        });
        // 提交异步任务
        Executors.newSingleThreadExecutor().submit(task);

        return task;
    }
}

CompletionStage

CompletionStage 是 JDK 1.8 提供的表示异步执行的其中一个阶段,可以在当前阶段完成后进入下一个阶段,典型的实现是 CompletableFuture。

使用 CompletableFuture 作为 controller 作为返回值,Spring 会将其适配为 DeferredResult,在当前阶段完成后设置异步处理的结果,从功能上来说强于 Callable,可以设置线程池,但不能设置回调和设置超时时间。示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public CompletionStage<String> test() {
        CompletionStage<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                return "this is a test";
            }
        }, Executors.newSingleThreadExecutor());

        return future;
    }
}

ResponseBodyEmitter、ResponseEntity<ResponseBodyEmitter>

ResponseBodyEmitter 类型的作用类似于 Servlet 异步处理原生的 API,支持用户多次发出响应,这个类型作为 controller 方法返回类型后,Spring 同样会将这个类型适配为 DeferredResult。这个类型支持异步处理回调、设置超时时间,指定线程池等。

ResponseEntity<ResponseBodyEmitter> 相比 ResponseBodyEmitter 多了设置响应码,响应头的能力。

ResponseBodyEmitter 示例代码如下:

@RestController
public class AsyncController {

    @GetMapping("/test")
    public ResponseBodyEmitter test() {

        ResponseBodyEmitter emitter = new ResponseBodyEmitter(5000L);

        // 异步线程池中执行耗时任务
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                // 设置异步处理回调
                emitter.onCompletion(() -> System.out.println("异步处理完成"));
                emitter.onTimeout(() -> System.out.println("异步处理"));
                emitter.onError((throwable) -> System.out.println("异步处理异常:" + throwable.getMessage()));

                // 模拟耗时操作
                Thread.sleep(3000L);

                // 发送响应
                emitter.send("this is ");
                emitter.send("a test");


                // 通知容器异步处理完成
                emitter.complete();
            }
        });

        return emitter;
    }
}

需要注意的是由于 Spring 需要等待 controller 方法返回后才能真正设置回调,因此如果异步任务如果在 controller 方法返回前就已经执行结束,回调将无法生效。

Spring MVC 异步处理方式总结

这里总结几种 controller 方法返回类型的异同,上述中的几种类型的适配关系可以如下图所示:

在这里插入图片描述

图中下面的类型可以适配到上面的类型,最终由 WebAsyncManager 使用来开启异步处理。

各类型功能异同如下表,可根据需求选择合适的类型进行异步处理。

类型 是否支持设置线程池 是否需要手动开启异步线程 是否支持超时设置 是否支持异步回调 是否支持多次输出响应
Callable
DeferredResult
StreamingResponseBody
WebAsyncTask
ListenableFuture 仅支持成功失败回调
CompletionStage
ResponseBodyEmitter

Spring 异步处理流程

首先 Spring 将按照正常的流程执行 controller 方法,方法返回后 Spring 处理和异步有关的几个类型值,然后开始异步处理。以 Callable 类型为例,处理这个返回值类型的代码如下:

public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
    	return Callable.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
    							  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    	if (returnValue == null) {
    		mavContainer.setRequestHandled(true);
    		return;
    	}

    	Callable<?> callable = (Callable<?>) returnValue;
    	// 开启异步处理
    	WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);
    }

}

Spring 先调用 WebAsyncUtils.getAsyncManager 方法获取异步管理器 WebAsyncManager,WebAsyncManager 是异步处理的核心类,WebAsyncManager 获取之后会将实例存储到 request 的属性中。代码如下:

public abstract class WebAsyncUtils {

	public static WebAsyncManager getAsyncManager(WebRequest webRequest) {
		int scope = RequestAttributes.SCOPE_REQUEST;
		WebAsyncManager asyncManager = null;
		Object asyncManagerAttr = webRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, scope);
		if (asyncManagerAttr instanceof WebAsyncManager) {
			asyncManager = (WebAsyncManager) asyncManagerAttr;
		}
		if (asyncManager == null) {
			asyncManager = new WebAsyncManager();
			// 将实例存储至 request 属性
			webRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager, scope);
		}
		return asyncManager;
	}

}

然后 Spring 调用 WebAsyncManager#startCallableProcessing(Callable<?>, Object...) 开始异步处理,包括设置回调、开启异步处理、执行异步任务等等,这里将用到 Servlet 原生的 API,由于代码较多,不再展示。执行异步任务后 Spring 会调用 AsyncContext#dispatch() 将请求重新派发到当前 controller。

当请求转发到当前 controller 时,RequestMappingHandlerAdapter 会再次执行 controller 方法,此时从 request 属性中取出 WebAsyncManager,发现已经产生异步处理的结果,然后对表示 controller 方法的 ServletInvocableHandlerMethod 加以包装,使其直接返回异步处理结果,后面和正常流程一样,最终将结果输出到客户端。这块代码可参考 RequestMappingHandlerAdapter#invokeHandlerMethod,不再具体展示。

 

参考:

 

posted @ 2022-01-11 23:09  残城碎梦  阅读(1206)  评论(0编辑  收藏  举报