2026/4/18 8:51:33
网站建设
项目流程
站长论坛 激活网站,许昌住房建设局网站,中石油七建设公司官网,微网站建设多少钱前言Servlet 3.0之前#xff1a;每一次Http请求都由一个线程从头到尾处理。Servlet 3.0之后#xff0c;提供了异步处理请求#xff1a;可以先释放容器分配给请求的线程与相关资源#xff0c;减轻系统负担#xff0c;从而增加服务的吞吐量。在springboot应用中#xff0c;…前言Servlet 3.0之前每一次Http请求都由一个线程从头到尾处理。Servlet 3.0之后提供了异步处理请求可以先释放容器分配给请求的线程与相关资源减轻系统负担从而增加服务的吞吐量。在springboot应用中可以有4种方式实现异步接口至于ResponseBodyEmitter、SseEmitter、StreamingResponseBody不在本文介绍内之后新写文章介绍AsyncContextCallableWebAsyncTaskDeferredResult第一中AsyncContext是Servlet层级的比较原生的方式本文不对此介绍一般都不使用它太麻烦了。本文着重介绍后面三种方式。特别说明服务端的异步或同步对于客户端而言是不可见的。不会因为服务端使用了异步接口的结果就和同步不一样了。另外对于单个请求而言使用异步接口会导致响应时间比同步大但不特别明显。具体后文分析。基于Callable实现Controller中返回一个java.util.concurrent.Callable包装的任何值都表示该接口是一个异步接口GetMapping(/testCallAble) public CallableString testCallAble() { return () - { Thread.sleep(40000); return hello; }; }服务器端的异步处理对客户端来说是不可见的。例如上述接口最终返回的客户端的是一个String和同步接口中直接返回String的效果是一样的。Callable处理过程如下控制器返回一个Callable。Spring MVC 调用request.startAsync()并将Callable提交给AsyncTaskExecutor以在单独的线程中进行处理。同时DispatcherServlet和所有过滤器退出 Servlet 容器线程但response保持打开状态。最终Callable产生结果Spring MVC将请求分派回Servlet容器以完成处理。再次调用DispatcherServlet并使用Callable异步生成的返回值继续处理。Callable默认使用SimpleAsyncTaskExecutor类来执行这个类非常简单而且没有重用线程。在实践中需要使用AsyncTaskExecutor类来对线程进行配置。基于WebAsyncTask实现Spring提供的WebAsyncTask是对Callable的包装提供了更强大的功能比如处理超时回调、错误回调、完成回调等。本质上和Callable区别不大但是由于它额外封装了一些事件的回调所有通常都使用WebAsyncTask而不是CallableGetMapping(/webAsyncTask) public WebAsyncTaskString webAsyncTask() { WebAsyncTaskString result new WebAsyncTask(30003, () - { return success; }); result.onTimeout(() - { log.info(timeout callback); return timeout callback; }); result.onCompletion(() - log.info(finish callback)); return result; }这里额外提一下WebAsyncTask可以配置一个超时时间这里配置的超时时间比全局配置的超时时间优先级都高会覆盖全局配置的超时时间。基于DeferredResult实现DeferredResult使用方式与Callable类似但在返回结果时不一样它返回的时实际结果可能没有生成实际的结果可能会在另外的线程里面设置到DeferredResult中去。//定义一个全局的变量用来存储DeferredResult对象 private MapString, DeferredResultString deferredResultMap new ConcurrentHashMap(); GetMapping(/testDeferredResult) public DeferredResultString testDeferredResult(){ DeferredResultString deferredResult new DeferredResult(); deferredResultMap.put(test, deferredResult); return deferredResult; }如果调用以上接口会发现客户端的请求一直是在pending状态——等待后端响应。这里我简单的将该接口返回的DeferredResult对象存放在了一个Map集合中实际应用中可以设计一个对象管理器来统一管理这些个对象。注意要考虑定时轮询或其他方式这些对象将已经处理过或无效的DeferredResult对象清理掉DeferredResult.isSetOrExpired方法可以判断是否还有效避免内存泄露。这里我又写了一个接口模拟GetMapping(/testSetDeferredResult) public String testSetDeferredResult() throws InterruptedException { DeferredResultString deferredResult deferredResultMap.get(test); boolean flag deferredResult.setResult(testSetDeferredResult); if(!flag){ log.info(结果已经被处理此次操作无效); } return ok; }其他线程修改DeferredResult的值首先是从之前存放DeferredResult的map中拿到DeferredResult的值然后设置它的返回值。当执行deferredResult.setResult之后可以看到之前pending状态的接口完成了响应得到的结果就是这里设置的值。这里也额外说下在返回DeferredResult时也可以设置超时时间这个时间的优先级也是大于全局设置的。另外判断DeferredResult是否有效只是一个简单的判断实际中判断有效的并不一定是有效的比如客户端取消了请求服务端是不知道的但是一般判断为无效的那肯定是无效了。DeferredResult处理过程如下控制器返回一个DeferredResult并将其保存在可以访问的内存队列或列表中。Spring MVC 调用request.startAsync()。同时DispatcherServlet和所有配置的过滤器退出请求处理线程但响应保持打开状态。应用程序从某个线程设置DeferredResultSpring MVC 将请求分派回 Servlet 容器。再次调用DispatcherServlet并使用异步生成的返回值继续处理。提供一个线程池异步请求不会一直占用请求的主线程tomcat容器中处理请求的线程而是通过一个其他的线程来处理异步任务。也正是如此在相同的最大请求数配置下异步请求由于迅速的释放了主线程所以才能提高吞吐量。这里提到一个其他线程那么这个其他线程我们一般都不适用默认的都是根据自身情况提供一个线程池供异步请求使用我给的参数都是测试用的实际中不可照搬Bean(mvcAsyncTaskExecutor) public AsyncTaskExecutor asyncTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); // 线程池维护线程的最少数量 // asyncServiceExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors() 1); executor.setCorePoolSize(5); // 线程池维护线程的最大数量 executor.setMaxPoolSize(10); // 线程池所使用的缓冲队列 executor.setQueueCapacity(10); // asyncServiceExecutor.prefersShortLivedTasks(); executor.setThreadNamePrefix(fyk-mvcAsyncTask-Thread-); // asyncServiceExecutor.setBeanName(TaskId taskId); // asyncServiceExecutor.setKeepAliveSeconds(20); //调用者执行 // asyncServiceExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); // 线程全部结束才关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 如果超过60s还没有销毁就强制销毁以确保应用最后能够被关闭而不是阻塞住 executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; }把这个线程池配置设置到异步请求配置中Configuration public class FykWebMvcConfigurer implements WebMvcConfigurer { Autowired Qualifier(mvcAsyncTaskExecutor) private AsyncTaskExecutor asyncTaskExecutor; Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { //异步操作的超时时间值为0或者更小表示永不超时 configurer.setDefaultTimeout(60001); configurer.setTaskExecutor(asyncTaskExecutor); } }什么时候使用异步请求异步请求能提高吞吐量这个是建立在相同配置这里的配置指的是最大连接数、最大工作线程数的情况下。因此并不是说任何接口都可以使用异步请求。比如一个请求是进行大量的计算总之就是在处理这个请求的业务方法时CPU是没有休息的这种情况使用异步请求就没有多大意义了因为这时的异步请求只是把一个任务从tomcat的工作线程搬到了另一个线程罢了。直接调大最大工作线程数配置也能到达要求。所以真正使用异步请求的场景应该是该请求的业务代码中大量的时间CPU是休息的比如在业务代码中请求其他系统的接口在其他系统响应之前CPU是阻塞等待的这个时候使用异步请求就可以释放tomcat的工作线程让释放的工作线程可以处理其他的请求从而提高吞吐量。由于异步请求增加了更多的线程切换同步请求是同一个工作线程一直处理所以理论上会增加接口的耗时。但这个耗时很短很短。