很多人都有过这种体验:明明一个接口逻辑挺简单,但就是慢,特别是涉及数据库查询、远程调用的时候,串起来一跑,秒变蜗牛。比如查用户详情,要查用户基本信息,还得拉订单,再去查积分,你顺序写下来,接口响应可能就接近一秒了,用户体验直接崩掉。
串行的写法
一般我们最常见的写法大概是这样:
public UserDetailDTO getUserDetail(Long userId) {
User user = userService.getUserById(userId);
List<Order> orders = orderService.getOrders(userId);
int points = pointsService.getPoints(userId);
return new UserDetailDTO(user, orders, points);
}
逻辑直观没问题,就是三个调用全是串行的。如果每个都要 300ms,总耗时差不多就是 900ms。
用 CompletableFuture 并行请求
其实这些调用之间并没有依赖关系,完全可以同时发出去,这时候CompletableFuture就能派上用场。我们只需要一个线程池,就能把三个调用变成并行的:
public UserDetailDTO getUserDetail(Long userId) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUserById(userId), executor);
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(
() -> orderService.getOrders(userId), executor);
CompletableFuture<Integer> pointsFuture = CompletableFuture.supplyAsync(
() -> pointsService.getPoints(userId), executor);
CompletableFuture.allOf(userFuture, ordersFuture, pointsFuture).join();
try {
returnnew UserDetailDTO(userFuture.get(), ordersFuture.get(), pointsFuture.get());
} catch (Exception e) {
thrownew RuntimeException(e);
}
}
这样三个请求并行发出,总耗时基本就是最慢的那个,大概 300ms 左右,性能直接翻倍。
异常处理
实际用的时候,不可能所有服务都百分百可用,有一个挂了怎么办?CompletableFuture给了我们优雅的处理方式,比如兜底返回:
userFuture = userFuture.exceptionally(ex -> {
log.error("查询用户失败", ex);
return new User(); // 返回一个默认对象,避免整个接口报错
});
这样就算某个子任务出错,也不会把整个接口拖死。
超时控制
光是并行还不够,有时候下游服务超时不返回,那接口还是会卡住。我们可以给CompletableFuture加上超时控制:
public static <T> T getWithTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
try {
return future.get(timeout, unit);
} catch (TimeoutException e) {
future.cancel(true); // 超时后取消任务
throw new RuntimeException("调用超时", e);
} catch (Exception e) {
throw new RuntimeException("任务执行失败", e);
}
}
用的时候就像这样:
User user = getWithTimeout(userFuture, 500, TimeUnit.MILLISECONDS);
List<Order> orders = getWithTimeout(ordersFuture, 500, TimeUnit.MILLISECONDS);
int points = getWithTimeout(pointsFuture, 500, TimeUnit.MILLISECONDS);
这样即使下游服务出问题,也不会把整个接口拖到 10 秒以上,前端能尽快得到响应。
接口慢,很多时候不是业务逻辑复杂,而是调用方式没优化。用CompletableFuture并行发请求,加上异常兜底和超时控制,接口性能基本能提升一大截。在微服务环境下尤其明显,一个请求要调好几个服务,如果还在串行等待,那真的是浪费时间。