news 2026/2/10 4:04:30

同步 vs 异步性能差10倍!SpringBoot 高吞吐接口实现终极方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
同步 vs 异步性能差10倍!SpringBoot 高吞吐接口实现终极方案

前言

Servlet 3.0之前:每一次Http请求都由一个线程从头到尾处理。

Servlet 3.0之后,提供了异步处理请求:可以先释放容器分配给请求的线程与相关资源,减轻系统负担,从而增加服务的吞吐量。

在springboot应用中,可以有4种方式实现异步接口(至于ResponseBodyEmitterSseEmitterStreamingResponseBody,不在本文介绍内,之后新写文章介绍):

  • AsyncContext

  • Callable

  • WebAsyncTask

  • DeferredResult

第一中AsyncContext是Servlet层级的,比较原生的方式,本文不对此介绍(一般都不使用它,太麻烦了)。本文着重介绍后面三种方式。

特别说明:服务端的异步或同步对于客户端而言是不可见的。不会因为服务端使用了异步,接口的结果就和同步不一样了。另外,对于单个请求而言,使用异步接口会导致响应时间比同步大,但不特别明显。具体后文分析。

基于Callable实现

Controller中,返回一个java.util.concurrent.Callable包装的任何值,都表示该接口是一个异步接口:

@GetMapping("/testCallAble") public Callable<String> 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而不是Callable

@GetMapping("/webAsyncTask") public WebAsyncTask<String> webAsyncTask() { WebAsyncTask<String> 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 Map<String, DeferredResult<String>> deferredResultMap = new ConcurrentHashMap<>(); @GetMapping("/testDeferredResult") public DeferredResult<String> testDeferredResult(){ DeferredResult<String> deferredResult = new DeferredResult<>(); deferredResultMap.put("test", deferredResult); return deferredResult; }

如果调用以上接口,会发现客户端的请求一直是在pending状态——等待后端响应。这里,我简单的将该接口返回的DeferredResult对象存放在了一个Map集合中,实际应用中可以设计一个对象管理器来统一管理这些个对象。

注意:要考虑定时轮询(或其他方式)这些对象,将已经处理过或无效的DeferredResult对象清理掉(DeferredResult.isSetOrExpired方法可以判断是否还有效),避免内存泄露。

这里我又写了一个接口,模拟

@GetMapping("/testSetDeferredResult") public String testSetDeferredResult() throws InterruptedException { DeferredResult<String> 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和所有配置的过滤器退出请求处理线程,但响应保持打开状态。

  • 应用程序从某个线程设置DeferredResult,Spring 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的工作线程,让释放的工作线程可以处理其他的请求,从而提高吞吐量。

由于异步请求增加了更多的线程切换(同步请求是同一个工作线程一直处理),所以理论上会增加接口的耗时。但,这个耗时很短很短。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 17:42:19

掌握这3个Dify调试工具隐藏功能,团队效率翻倍不是梦

第一章&#xff1a;Dify调试工具的核心价值Dify调试工具为开发者提供了一套完整的运行时洞察与问题排查机制&#xff0c;显著提升了AI应用开发的效率与稳定性。其核心价值不仅体现在快速定位错误&#xff0c;更在于对复杂逻辑链路的可视化追踪和实时数据监控。提升开发效率 实时…

作者头像 李华
网站建设 2026/2/9 9:57:37

Jupyter Notebook导出GLM-4.6V-Flash-WEB推理流程为PDF文档

Jupyter Notebook导出GLM-4.6V-Flash-WEB推理流程为PDF文档 在当前多模态AI应用快速落地的背景下&#xff0c;如何高效展示模型能力、固化实验过程并生成可交付成果&#xff0c;成为开发者面临的核心挑战之一。尤其是在智能客服、内容审核、教育演示等场景中&#xff0c;仅仅“…

作者头像 李华
网站建设 2026/2/9 20:38:14

Jupyter Notebook魔法命令调试GLM-4.6V-Flash-WEB性能瓶颈

Jupyter Notebook魔法命令调试GLM-4.6V-Flash-WEB性能瓶颈 在当今多模态AI快速渗透Web应用的背景下&#xff0c;一个现实问题日益凸显&#xff1a;即便模型具备强大的图文理解能力&#xff0c;一旦推理延迟超过200毫秒&#xff0c;用户体验就会明显下滑。尤其在图像问答、内容审…

作者头像 李华
网站建设 2026/2/4 11:41:20

Git Commit GPG签名保障GLM-4.6V-Flash-WEB贡献者身份真实

Git Commit GPG签名保障GLM-4.6V-Flash-WEB贡献者身份真实 在AI开源项目日益繁荣的今天&#xff0c;GLM系列模型的快速迭代吸引了大量开发者参与。但开放协作的背后&#xff0c;一个隐忧始终存在&#xff1a;我们如何确认每一次代码提交都来自真实的贡献者&#xff1f;尤其是在…

作者头像 李华
网站建设 2026/2/8 21:51:07

智谱新模型GLM-4.6V-Flash-WEB实战:快速部署与网页推理操作手册

智谱新模型GLM-4.6V-Flash-WEB实战&#xff1a;快速部署与网页推理操作手册 在当前AI应用加速落地的浪潮中&#xff0c;一个现实问题始终困扰着开发者&#xff1a;为什么很多先进的多模态大模型“看起来很强大”&#xff0c;却难以真正用起来&#xff1f; 答案往往出人意料地简…

作者头像 李华
网站建设 2026/2/9 23:13:13

HTML5 Video标签结合GLM-4.6V-Flash-WEB实现实时视频帧分析

HTML5 Video标签结合GLM-4.6V-Flash-WEB实现实时视频帧分析 在浏览器里跑一个能“看懂”视频的AI&#xff0c;曾经是前端工程师梦里的场景。如今&#xff0c;随着WebAssembly、WebGPU和轻量化大模型的发展&#xff0c;这已经不再是幻想——你不需要安装任何插件&#xff0c;也不…

作者头像 李华