在控制器中使用 @Async 和 CompletableFuture 可以提高我们 api 的性能吗?

Posted

技术标签:

【中文标题】在控制器中使用 @Async 和 CompletableFuture 可以提高我们 api 的性能吗?【英文标题】:is using @Async and CompletableFuture in controller can increase performance of our api? 【发布时间】:2021-03-15 03:03:58 【问题描述】:

我想要实现的是,通过以这种简单的方式使用多线程,在我的 RESTApi 控制器中使用 @Async 和 CompletableFuture 可以获得更好的性能吗?

这是我的工作,这是我的控制器:

@PostMapping("/store")
@Async
public CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> saveNewCategoryBPSJ(@Valid @RequestBody InputRequest<CategoryBPSJRequestDto> request) 
    
    CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();

    future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJService.save(request))));
    return future;

VS

@PostMapping("/store")
public ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>> saveNewCategoryBPSJ(@Valid @RequestBody InputRequest<CategoryBPSJRequestDto> request) 
    
    return ResponseEntity.ok(new ResponseRequest<>("okay", categoryBPSJService.save(request));

正如您在我的第一个控制器函数中看到的那样,我在函数响应中添加了 CompletableFuture,但在我的服务中,我确实保存在这一行中 categoryBPSJService.save(request) 不是异步的,只是一个看起来像这样的简单函数:

public CategoryBpsjResponseDto save(InputRequest<CategoryBPSJRequestDto> request) 
    CategoryBPSJRequestDto categoryBPSJDto = request.getObject();

    Boolean result = categoryBPSJRepository.existsCategoryBPSJBycategoryBPSJName(categoryBPSJDto.getCategoryBPSJName());

    if(result)
        throw new ResourceAlreadyExistException("Category BPSJ "+ categoryBPSJDto.getCategoryBPSJName() + " already exists!");
    

    CategoryBPSJ categoryBPSJ = new CategoryBPSJ();
    categoryBPSJ = map.DTOEntity(categoryBPSJDto);

    categoryBPSJ.setId(0L);
    categoryBPSJ.setIsDeleted(false);

    CategoryBPSJ newCategoryBPSJ = categoryBPSJRepository.save(categoryBPSJ);
    
    CategoryBpsjResponseDto categoryBpsjResponseDto = map.entityToDto(newCategoryBPSJ);

    return categoryBpsjResponseDto;


我只是返回带有 JPA 连接的简单对象,这样我的请求性能会提高吗?还是我错过了一些增加它的东西?或者我的控制器上有没有 CompletableFuture 和 @Async 都没有区别?

*注意:我的项目基于 java 13

【问题讨论】:

如果您尝试“提高性能”(这可能意味着几件事中的任何一个),您首先需要测量您的应用程序以确定任何性能问题所在。 我不认为这真的有用。如果您在应用服务器中,则 AS 有一个线程池来响应 Web 请求。您在这里所做的是释放 Web 线程,但将其移动到 Spring 使用的线程池中。所以对我来说没有优势。当您的服务需要等待外部资源时,使用异步可能很有用。如果您使用的是 Spring boot,请查看 spring webflux。 并非如此,您只是释放请求处理线程,理论上允许同时处理更多请求。它不会提高性能,因为处理需要相同的时间。同样@AsyncCompletableFuture 都是错误的,使用CompletableFuture 不要在控制器方法上使用@Async 哦,我的优势只是太释放请求处理线程对吗? @M.Deinum 但没有提高我的控制器响应速度? “释放请求处理线程”有什么好处?我可以认为这是一个加分项吗? 如果你有很多并发用户和很多短期任务和一些长期运行。如果 tomcat 忙于处理那些长时间运行的任务,它就无法处理短暂的任务,因为没有更多的线程来处理请求。将这些请求卸载到另一个线程池将很快释放这些线程,以便它可以继续处理其他请求。 【参考方案1】:

使用 CompletableFuture 不会神奇地提高服务器的性能。

如果您使用Spring MVC,通常在 Jetty 或 Tomcat 之上构建在 Servlet API 之上,则每个请求将有一个线程。这些线程从中获取的池通常非常大,因此您可以拥有相当数量的并发请求。在这里,阻塞请求线程不是问题,因为该线程无论如何都只处理单个请求,这意味着其他请求不会被阻塞(除非池中不再有可用线程)。这意味着,您的 IO 可以阻塞,您的代码可以 是同步的。

如果您使用Spring WebFlux,通常在 Netty 之上,请求将作为消息/事件处理:一个线程可以处理多个请求,这可以减少池的大小(线程很昂贵)。在这种情况下,线程阻塞一个问题,因为它可以/将导致其他请求等待 IO 完成。这意味着,您的 IO 必须是非阻塞的,您的代码必须是异步的,以便线程可以被释放并“同时”处理另一个请求,而不仅仅是等待空闲以完成操作。仅供参考,这种反应式堆栈看起来很吸引人,但由于代码库的异步特性,它还有许多其他需要注意的缺点。

JPA 是阻塞的,因为它依赖于 JDBC(阻塞在 IO 上)。这意味着,将 JPA 与 Spring WebFlux 一起使用没有多大意义,应该避免,因为它违背了“不阻塞请求线程”的原则。人们已经找到了解决方法(例如,从另一个线程池中运行 SQL 查询),但这并不能真正解决根本问题:IO 将阻塞,可能/将会发生争用。人们正在为 Java 开发异步 SQL 驱动程序(例如 Spring Data R2DBC 和底层供应商特定的驱动程序),例如可以在 WebFlux 代码库中使用。 Oracle 也开始开发自己的异步驱动程序 ADBA,但 they abandoned the project 因为他们通过 Project Loom 专注于 fibers(这可能很快会完全改变 Java 中处理并发的方式)。

您似乎正在使用 Spring MVC,这意味着依赖于每个请求的线程模型。只是在你的代码中删除 CompletableFuture 不会改善事情。假设您将所有服务层逻辑委托给默认请求线程池之外的另一个线程池:您的请求线程将可用,是的,但现在将在您的另一个线程池上发生争用,这意味着您将只是转移您的问题周围。

有些情况可能仍然很有趣,可以推迟到另一个池,例如计算密集型操作(如密码哈希),或某些会触发大量(阻塞)IO 的操作等,但请注意仍然可能发生争用,这意味着请求仍然可能被阻塞/等待。

如果您确实观察到代码库的性能问题,请首先对其进行分析。使用像YourKit(许多其他可用)这样的工具,甚至像NewRelic(还有许多其他可用)这样的APM。了解瓶颈在哪里,解决最坏的问题,重复。话虽如此,一些常见的嫌疑人:太多的 IO(尤其是 JPA,例如select n+1),太多的序列化/反序列化(尤其是 JPA,例如eager fetching)。基本上,JPA 是 通常的怀疑对象:它是一个强大的工具,但很容易配置错误,您需要 think SQL 以使其正确,恕我直言。我在开发时强烈推荐logging the generated SQL queries,你可能会感到惊讶。 Vlad Mihalcea's blog 是 JPA 相关内容的好资源。有趣的阅​​读:OrmHate by Martin Fowler。


关于您的特定代码 sn-p,假设您将使用没有 Spring 的 @Async 支持的普通 Java:

CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();
future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJService.save(request))));
return future;

这不会使categoryBPSJService.save(request) 异步运行。如果您将代码拆分一下,它将变得更加明显:

CategoryBpsjResponseDto categoryBPSJ = categoryBPSJService.save(request)
CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();
future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));
return future;

看看这里发生了什么? categoryBPSJ 将被同步调用,然后您将创建一个已经完成的未来来保存结果。如果您真的想在这里使用 CompletableFuture,则必须使用供应商:

CompletableFuture<CategoryBpsjResponseDto> future = CompletableFuture.supplyAsync(
    () -> categoryBPSJService.save(request),
    someExecutor
);
return future.thenApply(categoryBPSJ -> ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));

Spring's @Async 基本上只是上面的语法糖,使用非此即彼。出于技术 AOP/代理的原因,使用 @Async 注释的方法确实需要返回 CompletableFuture,在这种情况下返回已经完成的未来就可以了:Spring 无论如何都会让它在执行程序中运行。服务层通常是“异步”的,但控制器只是消费和组合返回的未来:

CompletableFuture<CategoryBpsjResponseDto> = categoryBPSJService.save(request);
return future.thenApply(categoryBPSJ -> ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));

通过调试代码确保它的行为符合您的预期,IDE 会显示当前哪个线程被断点阻塞。


旁注:这是我从阻塞与非阻塞、MVC 与 WebFlux、同步与异步等方面理解的简化总结。这很肤浅,我的一些观点可能不够具体,无法 100% 正确。

【讨论】:

哇,这就是我想要的解释。你满足了我的好奇心,谢谢 sp00m

以上是关于在控制器中使用 @Async 和 CompletableFuture 可以提高我们 api 的性能吗?的主要内容,如果未能解决你的问题,请参考以下文章

在控制器中使用 @Async 和 CompletableFuture 可以提高我们 api 的性能吗?

我们需要在控制器中使用 async/await 关键字吗?

何时使用 Spring @Async vs Callable 控制器(异步控制器,servlet 3)

AJAX中同步和异步的区别和使用场景

在 web api 控制器(.net 核心)中使用 async/await 或任务

如何用 async 控制流程