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

我试图实现的是,我能否通过使用@Async和ComppletableFuture来获得更好的性能,从而通过以这种简单的方式使用多线程来控制我的RESTApi?

这是我所做的,这是我的控制器:

@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;
}

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

正如你在我的第一个控制器函数中看到的,我在我的函数响应上添加了ComppletableFuture,但是在我的服务中,我确实保存了这一行不是异步的,只是一个简单的函数,看起来像这样: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的


答案 1

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

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

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

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

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

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

如果确实观察到代码库存在性能问题,请先对其进行分析。使用像YourKit这样的工具(许多其他可用的工具),甚至是像NewRelic这样的APM(许多其他可用的工具)。了解瓶颈在哪里,修复最坏的情况,重复。话虽如此,一些常见的怀疑:太多的IO(特别是对于JPA,例如选择n + 1),太多的序列化/反序列化(特别是对于JPA,例如急切的获取)。基本上,JPA是通常嫌疑人:它是一个功能强大的工具,但很容易配置错误,你需要考虑SQL才能让它正确恕我直言。我强烈建议在开发时记录生成的SQL查询,您可能会感到惊讶。Vlad Mihalcea的博客是JPA相关内容的良好资源。有趣的阅读:马丁·福勒的OrmHate


关于你的特定代码片段,假设你正在没有Spring的支持的情况下使用普通的Java:@Async

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;

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

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

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

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

通过调试代码,确保一切按预期运行,IDE 显示当前被断点阻止的线程。


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


答案 2

推荐