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

is using @Async and CompletableFuture in controller can increase performance of our api?

我想要实现的是,通过以这种简单的方式使用多线程,在我的 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;

}

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

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

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

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

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

JPA 是阻塞的,因为它依赖于 JDBC(在 IOs 上阻塞)。这意味着,将 JPA 与 Spring WebFlux 一起使用没有多大意义,应该避免,因为它违反了“不阻塞请求线程”的原则。人们找到了解决方法(例如 运行 从另一个线程池中查询 SQL),但这并没有真正解决根本问题:IOs 会阻塞,争用 can/will 发生。人们正在为 Java 开发异步 SQL 驱动程序(例如 Spring Data R2DBC and the underlying vendor-specific drivers), which can be used from within a WebFlux codebase for instance. Oracle started working on their own asynchronous driver too, ADBA, but they abandoned the project because of their focus on fibers via Project Loom(这可能很快就会完全改变 Java 中处理并发的方式)。

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

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

如果您 观察到代码库的性能问题,首先分析它。使用 YourKit (many others available) or even APMs like NewRelic (many others available as well). Understand where the bottlenecks are, fix the worsts, repeat. That being said, some usual suspects: too many IOs (especially with JPA, e.g. select n+1), too many serialisations/deserialisations (especially with JPA, e.g. eager fetching). Basically, JPA is the usual suspect: it's a powerful tool, but very easy to misconfigure, you need to think SQL to get it right IMHO. I highly recommend logging the generated SQL queries when developing, you might be surprised. Vlad Mihalcea's blog is a good resource for JPA related things. Interesting read as well: OrmHate by Martin Fowler.

等工具

关于您的特定代码片段,假设您要在没有 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 基本上只是上面的语法糖,使用 either/or。出于技术 AOP/proxying 原因,使用 @Async 注释的方法确实需要 return CompletableFuture,在这种情况下 return 已经完成的未来很好:Spring 无论如何都会在执行程序中使其成为 运行 。尽管服务层通常是“异步”层,但控制器只是在 returned 未来消费和组合:

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

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


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