Spring Boot API性能
Spring Boot | API 性能优化 | CompletableFuture在当今快速变化的API开发世界中,性能是决定用户体验的关键因素。高延迟的API可能让用户感到烦躁,甚至导致收入流失。在这篇文章里,我会讨论使用Java的CompletableFuture
来提升API性能的一个实用方案,并介绍了一个基于Spring Boot的概念验证(POC)。
当构建从数据库或外部服务等来源检索数据的微服务或API时,通常会遇到顺序的调用请求,这些调用请求会阻塞执行线程的运行,直到每个响应被返回。这样的同步方式会严重降低性能,尤其是在存在延迟(例如网络延迟或因Thread.sleep()
引起的有意延迟)时。
为了展示这一点,我创建了一个简单的POC(概念验证),包含两个API接口来从后端获取产品详情,包括价格和库存信息。第一个API实现采用同步方式,意味着它依次等待每个操作完成。这将导致响应时间显著增加,特别是在引入人工延迟(如Thread.sleep()
)的情况下。
POC(点对点通信)由两个 API 组成,每个 API 负责检索产品详情,包括库存和价格。具体来说,这些 API 是按照以下结构实现的。
- 数据仓库:管理数据访问,获取产品、价格和库存数据。
- 服务:包含业务逻辑,协调从数据仓库中获取数据。
- 门面:作为一层来暴露服务的功能,其中一个实现使用
CompletableFuture
异步处理,另一个则没有。 - 控制器:提供API端点。
CompletableFuture
的同步实现
在第一个阶段中,代码逐个阻塞获取产品详情、库存量和价格。这里是一个简化的版本,
首先,从仓库获取产品详情。接着,获取价格信息和库存数据。然后,返回产品详情汇总,包含价格和库存信息。最后,完成上述步骤。
因为这些操作是顺序执行的,任何一个步骤的延迟(例如网络延迟或 Thread.sleep()
)都会阻塞后续的操作,从而使得 API 的响应时间变慢。
CompletableFuture
的异步处理方式
在第二个立面中,我使用 CompletableFuture
实现了相同的 API 逻辑,进行异步非阻塞调用。每个步骤(获取产品、价格和库存信息)并行执行,从而减少了整体等待时间。
这里是一个简化了的CompletableFuture
如何提升性能的例子:
- 异步执行:使用
CompletableFuture.supplyAsync()
,我同时异步获取产品、价格和库存的数据。 - 非阻塞调用:当某个调用还在等待响应时,其他调用可以继续处理。
- 合并结果:当所有操作完成后,我使用
CompletableFuture.allOf()
将所有结果作为一个单一响应返回。
下面是一步步的实施方法。
步骤一:数据库模式和领域模型我们将使用一个基本的框架来存储产品、价格和库存信息。我们的应用程序会访问这些信息来获取数据。
以下SQL脚本用于创建商品数据库表和定义表间关系。
-- 创建一个名为category的表,包含以下字段
CREATE TABLE `category` (
`id` bigint PRIMARY KEY,
`name` varchar(255),
`type` varchar(255),
`created_at` timestamp,
`created_by` varchar(255),
`modified_at` timestamp,
`modified_by` varchar(255),
`status` varchar(255)
);
-- 创建一个名为products的表,包含以下字段
CREATE TABLE `products` (
`id` bigint PRIMARY KEY,
`category_id` bigint,
`name` varchar(255),
`description` text,
`created_at` timestamp,
`created_by` varchar(255),
`modified_at` timestamp,
`modified_by` varchar(255),
`status` varchar(255)
);
-- 创建一个名为price的表,包含以下字段
CREATE TABLE `price` (
`id` bigint PRIMARY KEY,
`product_id` bigint,
`price` double,
`valid_from` timestamp,
`valid_to` timestamp,
`created_at` timestamp,
`created_by` varchar(255),
`modified_at` timestamp,
`modified_by` varchar(255),
`status` varchar(255)
);
-- 创建一个名为inventory的表,包含以下字段
CREATE TABLE `inventory` (
`id` bigint PRIMARY KEY,
`product_id` bigint,
`warehouse_id` bigint,
`available_quantity` integer,
`reserved_quantity` integer,
`created_at` timestamp,
`created_by` varchar(255),
`modified_at` timestamp,
`modified_by` varchar(255),
`status` varchar(255)
);
-- 在inventory表中添加product_id的外键约束,引用products表中的id
ALTER TABLE `inventory` ADD FOREIGN KEY (`product_id`) REFERENCES `products` (`id`);
-- 在products表中添加category_id的外键约束,引用category表中的id
ALTER TABLE `products` ADD FOREIGN KEY (`category_id`) REFERENCES `category` (`id`);
-- 在price表中添加product_id的外键约束,引用products表中的id
ALTER TABLE `price` ADD FOREIGN KEY (`product_id`) REFERENCES `products` (`id`);
步骤 2: 存储库层
第二步:仓库模块
创建三个存储库来与数据库表进行交互。
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
@Repository
public interface 价格Repository接口 extends JpaRepository<Price, Long> {
Optional<Price> findBy产品ID(Long 产品ID);
}
@Repository
public interface 库存Repository extends JpaRepository<Inventory, Long> {
Optional<Inventory> findBy产品ID(Long 产品ID);
}
我们将要定义一个服务来协调处理对代码仓库的调用。
产品服务模块public interface 产品服务接口 ProductService {
Optional<Product> findById(Long id);
}
@Service
public class 产品服务实现类 ProductServiceImpl implements ProductService {
private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);
private final ProductRepository productRepository;
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Optional<Product> findById(Long id) {
log.info("通过ID获取产品: {}", id);
addDelay();
return productRepository.findById(id);
}
private void addDelay() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
价格服务功能
public interface 价格服务接口 {
Optional<价格> 根据产品ID获取价格(Long 产品ID);
}
@Service
public class 价格服务实现 implements 价格服务接口 {
private static final 日志记录器 日志 = LoggerFactory.getLogger(价格服务实现.class);
private final 价格存储库 价格仓库;
public 价格服务实现(价格存储库 价格仓库) {
this.价格仓库 = 价格仓库;
}
@Override
public Optional<价格> 根据产品ID获取价格(Long 产品ID) {
日志.info("正在查询产品ID {} 的价格", 产品ID);
添加延迟时间();
return 价格仓库.根据产品ID查找价格(产品ID);
}
private void 添加延迟时间() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
库存管理服务
public interface InventoryService {
Optional<Inventory> getInventoryByProductId(Long productId);
}
@Service
public class InventoryServiceImpl implements InventoryService {
private static final Logger log = LoggerFactory.getLogger(InventoryServiceImpl.class);
private final InventoryRepository inventoryRepository;
public InventoryServiceImpl(InventoryRepository inventoryRepository) {
this.inventoryRepository = inventoryRepository;
}
@Override
public Optional<Inventory> getInventoryByProductId(Long productId) {
log.info("日志记录正在获取 productId {} 对应的库存信息", productId);
addDelay();
return inventoryRepository.findByProductId(productId);
}
private void addDelay() {
try {
// 延迟2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
// 延迟2秒
throw new RuntimeException(e);
}
}
}
第四步:外观层(Facade Layer)
外观负责管理服务的业务逻辑。我们分别为同步和异步情况设置了单独的外观。
产品 — 同步功能 @Component
public class ProductFacade {
private static final Logger log = LoggerFactory.getLogger(ProductFacade.class);
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
public ProductFacade(ProductService productService, InventoryService inventoryService, PriceService priceService) {
this.productService = productService;
this.inventoryService = inventoryService;
this.priceService = priceService;
}
/**
* 获取产品详细信息的门面,用于productId {}
*
* @param productId 输入的productId
* @return ProductDetailDTO
*/
public Optional<ProductDetailDTO> getProductDetail(Long productId) {
log.info("获取产品详细信息的门面,用于productId {}", productId);
Optional<Product> productOpt = productService.findById(productId);
if (productOpt.isEmpty()) {
return Optional.empty();
}
// 获取产品的价格信息
Optional<Price> priceOpt = priceService.getPriceByProductId(productId);
// 获取产品的库存信息
Optional<Inventory> inventoryOpt = inventoryService.getInventoryByProductId(productId);
// 如果产品存在,则将详情合并为产品详细信息
return productOpt.map(product -> {
Price price = priceOpt.orElse(null);
Inventory inventory = inventoryOpt.orElse(null);
assert inventory != null;
assert price != null;
return new ProductDetailDTO(productId, product.getCategory().getName(),
product.getName(), product.getDescription(), inventory.getAvailableQuantity(),
price.getPrice(), product.getStatus());
});
}
}
ProductFacade — 异步功能
@Component
public class ProductAsyncFacade {
private static final Logger log = LoggerFactory.getLogger(ProductAsyncFacade.class);
private final ProductService productService;
private final InventoryService inventoryService;
private final PriceService priceService;
public ProductAsyncFacade(ProductService productService, InventoryService inventoryService, PriceService priceService) {
this.productService = productService;
this.inventoryService = inventoryService;
this.priceService = priceService;
}
// 通过 productId 获取商品详情的 Future
private CompletableFuture<Optional<Product>> getProductById(Long productId) {
return CompletableFuture.supplyAsync(() -> productService.findById(productId));
}
// 通过 productId 获取价格详情的 Future
public CompletableFuture<Optional<Price>> getPriceByProductId(Long productId) {
return CompletableFuture.supplyAsync(() -> priceService.getPriceByProductId(productId));
}
// 通过 productId 获取库存详情的 Future
public CompletableFuture<Optional<Inventory>> getInventoryByProductId(Long productId) {
return CompletableFuture.supplyAsync(() -> inventoryService.getInventoryByProductId(productId));
}
/**
* 异步获取商品详情
*
* @param productId 输入的 productId
* @return ProductDetailsDTO
*/
public Optional<ProductDetailDTO> getProductDetailById(Long productId) {
// 异步获取所有数据
CompletableFuture<Optional<Product>> productFuture = getProductById(productId);
CompletableFuture<Optional<Price>> priceFuture = getPriceByProductId(productId);
CompletableFuture<Optional<Inventory>> inventoryFuture = getInventoryByProductId(productId);
// 等待所有 Future 完成后再继续执行
CompletableFuture.allOf(productFuture, priceFuture, inventoryFuture).join();
// 合并所有获取的结果
Optional<Product> productOpt = productFuture.join();
Optional<Price> priceOpt = priceFuture.join();
Optional<Inventory> inventoryOpt = inventoryFuture.join();
int availableQuantity = inventoryOpt.map(Inventory::getAvailableQuantity).orElse(0);
double price = priceOpt.map(Price::getPrice).orElse(0.0);
// 构造并返回 ProductDetail 实例
return productOpt.map(product -> new ProductDetailDTO(productId, product.getCategory().getName(),
product.getName(), product.getDescription(), availableQuantity,
price, product.getStatus()));
}
}
步骤 5: 控制层
控制器提供了两个 API:一个同步的 API,一个异步的 API。
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
private final ProductFacade productFacade;
private final ProductAsyncFacade productAsyncFacade;
public ProductController(ProductFacade productFacade, ProductAsyncFacade productAsyncFacade) {
this.productFacade = productFacade;
this.productAsyncFacade = productAsyncFacade;
}
@GetMapping("/{id}/sync")
public ResponseEntity<ProductDetailDTO> getProductSync(@PathVariable Long id) {
log.info("同步获取产品请求:{}", id);
return ResponseEntity.ok(productFacade.getProductDetail(id).orElseThrow());
}
@GetMapping("/{id}/async")
public ResponseEntity<ProductDetailDTO> getProductAsync(@PathVariable Long id) {
log.info("异步获取产品请求:{}", id);
return ResponseEntity.ok(productAsyncFacade.getProductDetailById(id).orElseThrow());
}
}
第 6 步:模型类 (Model 类)。
ProductDetailDTO
类将包含产品、价格和库存的相关信息。
@Data
@AllArgsConstructor
@NoArgsConstructor
// 商品详情数据传输对象
public class ProductDetailDTO {
// 商品ID
private Long id;
// 商品类别名称
private String categoryName;
// 商品名称
private String name;
// 商品描述
private String description;
// 可用库存数量
private Integer availableQuantity;
// 商品价格
private Double price;
// 商品状态
private String status;
}
第 7 步:响应时间比较
为了测试并比较这两个API的响应时间,我在同步服务端引入了Thread.sleep(2000)
毫秒来模拟以现实世界的延迟。
curl --location 'http://localhost:8080/api/v1/products/101/sync'
这行命令用于同步产品ID为101的数据。
异步 API: 2.22 s
运行以下命令来获取异步产品数据:
curl --location 'http://localhost:8080/api/v1/products/101/async'
观察记录
使用 CompletableFuture
的异步 API 通过并行执行显著缩短了响应时间。即使有 2 秒的延迟,异步版本仅耗时 2.22 秒,而同步版本则耗时 6.57 秒以上。
你可以在我的 GitHub 仓库中找到这个例子的完整源代码文件,你可以在那里找到它。
https://github.com/palmurugan/api-performance (API性能测试)
结论通过使用CompletableFuture
,我们可以使API调用异步且非阻塞,这可以显著提升性能,特别是在涉及多个数据源的情况下更为明显。这种方法简单易实现,并提供明显的性能提升,因此,它成为优化慢速或资源密集型API的首选方案。
共同学习,写下你的评论
评论加载中...
作者其他优质文章