前言
之前从 Elasticsearch 源码学到了线程池配置的设计思想,但 ES 的代码太底层,难以直接迁移到业务系统。今天在 Guava 框架中找到了更实用的方案:相比 CompletableFuture 复杂的链式调用,ListenableFuture 提供了更清晰的并发编排模式。
一、为什么选择 Guava ListenableFuture
CompletableFuture 的痛点
java
// allOf 返回 Void,还要手动 join
CompletableFuture.allOf(productFuture, inventoryFuture)
.thenApply(v -> {
Product product = productFuture.join(); // 还要再次 join
Inventory inventory = inventoryFuture.join();
return new ProductDetailDTO(product, inventory);
});
Guava 的优势
java
// 提交任务
ListenableFuture<Product> productFuture = executor.submit(() ->
productService.getProduct(id));
ListenableFuture<Inventory> inventoryFuture = executor.submit(() ->
inventoryService.getInventory(id));
// 等待全部成功后组装
return Futures.whenAllSucceed(productFuture, inventoryFuture)
.call(() -> {
Product product = Futures.getDone(productFuture);
Inventory inventory = Futures.getDone(inventoryFuture);
return new ProductDetailDTO(product, inventory);
}, executor);
核心优势:
- 职责分离:提交和处理分开
- 类型安全 :
getDone()直接返回结果 - 语义清晰 :
whenAllSucceed明确表达意图
二、业务场景:电商商品详情页
用户访问商品详情页,需要并行查询商品服务和库存服务:
graph TB
A[GET /product/123] --> B[ProductFacade]
B --> C[商品服务 80ms]
B --> D[库存服务 60ms]
C --> E[组装结果]
D --> E
E --> F[返回DTO]
- 串行耗时:80ms + 60ms = 140ms
- 并行耗时:max(80ms, 60ms) ≈ 80ms
三、完整代码实现
数据模型
java
// 商品信息
static class Product {
private final String id;
private final String name;
private final int priceInCents;
Product(String id, String name, int priceInCents) {
this.id = id;
this.name = name;
this.priceInCents = priceInCents;
}
String getId() { return id; }
String getName() { return name; }
int getPriceInCents() { return priceInCents; }
}
// 库存信息
static class Inventory {
private final String productId;
private final int quantity;
Inventory(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
String getProductId() { return productId; }
int getQuantity() { return quantity; }
}
// 返回的 DTO
static class ProductDetailDTO {
private final String productId;
private final String name;
private final int priceInCents;
private final int quantity;
ProductDetailDTO(String productId, String name, int priceInCents, int quantity) {
this.productId = productId;
this.name = name;
this.priceInCents = priceInCents;
this.quantity = quantity;
}
@Override
public String toString() {
return "ProductDetailDTO{" +
"productId='" + productId + '\'' +
", name='" + name + '\'' +
", priceInCents=" + priceInCents +
", quantity=" + quantity +
'}';
}
}
模拟服务
java
static class ProductService {
Product getProduct(String productId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(80); // 模拟网络调用
return new Product(productId, "MacBook Pro 14", 149900);
}
}
static class InventoryService {
Inventory getInventory(String productId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(60); // 模拟网络调用
return new Inventory(productId, 42);
}
}
核心门面类
java
static class ProductFacade {
private final ProductService productService;
private final InventoryService inventoryService;
private final ListeningExecutorService executor;
ProductFacade(ProductService productService,
InventoryService inventoryService,
ListeningExecutorService executor) {
this.productService = productService;
this.inventoryService = inventoryService;
this.executor = executor;
}
/**
* 并行获取商品信息和库存信息,然后组装成 ProductDetailDTO
*/
ListenableFuture<ProductDetailDTO> getProductDetail(final String productId) {
// 步骤1:Fan-Out 并行查询
ListenableFuture<Product> productFuture = executor.submit(
new Callable<Product>() {
@Override
public Product call() throws Exception {
return productService.getProduct(productId);
}
}
);
ListenableFuture<Inventory> inventoryFuture = executor.submit(
new Callable<Inventory>() {
@Override
public Inventory call() throws Exception {
return inventoryService.getInventory(productId);
}
}
);
// 步骤2:Fan-In 汇总结果
// whenAllSucceed:只有两个 Future 都成功,才执行 call 里的逻辑
return Futures.whenAllSucceed(productFuture, inventoryFuture)
.call(new Callable<ProductDetailDTO>() {
@Override
public ProductDetailDTO call() throws Exception {
// getDone:安全获取已完成的 Future 结果
Product product = Futures.getDone(productFuture);
Inventory inventory = Futures.getDone(inventoryFuture);
return new ProductDetailDTO(
product.getId(),
product.getName(),
product.getPriceInCents(),
inventory.getQuantity()
);
}
}, executor);
}
}
关键点:
executor.submit(Callable):提交任务到线程池异步执行,立即返回ListenableFutureFutures.whenAllSucceed(f1, f2):等待所有 Future 都成功完成,任何一个失败整体就失败Futures.getDone(future):安全获取已完成的结果,因为whenAllSucceed保证了成功
程序入口
java
public static void main(String[] args) throws Exception {
// 创建线程池
ListeningExecutorService executor = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(4)
);
// 初始化服务
ProductService productService = new ProductService();
InventoryService inventoryService = new InventoryService();
ProductFacade facade = new ProductFacade(
productService, inventoryService, executor
);
String productId = "P123";
long start = System.nanoTime();
// 发起并行查询
ListenableFuture<ProductDetailDTO> future = facade.getProductDetail(productId);
// 阻塞等待结果(Demo 演示用,实际 Web 场景可以用回调异步处理)
ProductDetailDTO dto = future.get();
long end = System.nanoTime();
long costMillis = (end - start) / 1_000_000L;
System.out.println("Result = " + dto);
System.out.println("Cost = " + costMillis + " ms");
executor.shutdown();
}
执行结果:
ini
Result = ProductDetailDTO{productId='P123', name='MacBook Pro 14', priceInCents=149900, quantity=42}
Cost = 85 ms
四、执行流程
时序图
sequenceDiagram
participant C as Controller
participant F as ProductFacade
participant E as 线程池
participant PS as ProductService
participant IS as InventoryService
C->>F: getProductDetail("P123")
F->>E: submit(查商品)
F->>E: submit(查库存)
par 并行执行
E->>PS: getProduct("P123")
PS-->>E: Product(80ms)
and
E->>IS: getInventory("P123")
IS-->>E: Inventory(60ms)
end
E->>F: 两个结果都完成
F->>F: 组装 ProductDetailDTO
F-->>C: 返回结果(~85ms)
Fan-Out / Fan-In 模式
graph LR
A[请求] --> B[Fan-Out]
B --> C[任务1]
B --> D[任务2]
C --> E[Fan-In]
D --> E
E --> F[结果]
style B fill:#e1f5ff
style E fill:#fff4e1
这是并发编程的经典模式:
- Fan-Out:将一个任务拆分成多个并行子任务
- Fan-In:等待所有子任务完成后汇总结果
五、与 Elasticsearch 的对比
ES 的场景:多索引并行查询
graph LR
A[搜索请求] --> B[logs-2024-01]
A --> C[logs-2024-02]
A --> D[logs-2024-03]
B --> E[合并排序]
C --> E
D --> E
E --> F[Top 10]
电商场景:多服务并行查询
graph LR
A[详情请求] --> B[商品服务]
A --> C[库存服务]
B --> D[简单组装]
C --> D
D --> E[返回DTO]
共同点
| 维度 | Elasticsearch | 电商商品详情 |
|---|---|---|
| 模式 | Fan-Out / Fan-In | Fan-Out / Fan-In |
| 并行目标 | 多个索引/分片 | 多个微服务 |
| 汇总方式 | 合并排序 | 简单组装 |
| 核心诉求 | 正确性 + 性能 | 响应时间 |
虽然场景不同,但底层思想一致:
- 分解:把大任务拆成多个独立小任务
- 并行:利用线程池同时执行
- 汇总:等待全部完成后组装结果
六、更多应用场景
订单详情页
java
ListenableFuture<Order> orderFuture = getOrder(orderId);
ListenableFuture<User> userFuture = getUser(userId);
ListenableFuture<List<OrderItem>> itemsFuture = getOrderItems(orderId);
ListenableFuture<Logistics> logisticsFuture = getLogistics(orderId);
return Futures.whenAllSucceed(orderFuture, userFuture, itemsFuture, logisticsFuture)
.call(() -> new OrderDetailDTO(...), executor);
用户个人中心
java
// 并行查询:用户信息、订单统计、优惠券、积分
ListenableFuture<UserInfo> userFuture = getUserInfo(userId);
ListenableFuture<OrderStats> statsFuture = getOrderStats(userId);
ListenableFuture<List<Coupon>> couponFuture = getCoupons(userId);
ListenableFuture<Points> pointsFuture = getPoints(userId);
return Futures.whenAllSucceed(userFuture, statsFuture, couponFuture, pointsFuture)
.call(() -> new UserCenterVO(...), executor);
数据报表
java
// 并行查询:销售数据、用户数据、库存数据
ListenableFuture<SalesData> salesFuture = getSalesData(startDate, endDate);
ListenableFuture<UserData> userDataFuture = getUserData(startDate, endDate);
ListenableFuture<InventoryData> inventoryFuture = getInventoryData();
return Futures.whenAllSucceed(salesFuture, userDataFuture, inventoryFuture)
.call(() -> new ReportVO(...), executor);
七、异常处理与降级
单个服务降级
java
// 使用 Futures.catching 处理异常
ListenableFuture<Product> productWithFallback = Futures.catching(
productFuture,
Exception.class,
ex -> {
log.error("查询商品失败,使用默认值", ex);
return getDefaultProduct(productId);
},
executor
);
超时控制
java
// 使用 Futures.withTimeout 添加超时
ListenableFuture<Product> productFuture = Futures.withTimeout(
executor.submit(() -> productService.getProduct(id)),
2, TimeUnit.SECONDS,
scheduledExecutor // 需要一个 ScheduledExecutorService
);
部分失败允许
如果允许部分查询失败,可以使用 Futures.successfulAsList():
java
// 即使部分失败,也返回成功的结果(失败的为 null)
ListenableFuture<List<Object>> results = Futures.successfulAsList(
productFuture,
inventoryFuture
);
return Futures.transform(results, list -> {
Product product = (Product) list.get(0);
Inventory inventory = (Inventory) list.get(1);
// 处理可能为 null 的情况
if (product == null) product = getDefaultProduct();
if (inventory == null) inventory = getDefaultInventory();
return new ProductDetailDTO(...);
}, executor);
八、生产环境配置建议
线程池配置
基于 ES 的经验(详见我的上一篇文章),推荐配置:
java
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
processors, // 核心线程数 = CPU 核心数
processors * 2, // 最大线程数(IO 密集型)
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(256 * processors * 2), // 有理论依据的队列大小
new ThreadFactoryBuilder()
.setNameFormat("ProductQuery-%d") // 线程命名
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 业务系统用降级策略
);
ListeningExecutorService executor = MoreExecutors.listeningDecorator(threadPool);
Spring Bean 配置
java
@Configuration
public class ExecutorConfig {
@Bean("productQueryExecutor")
public ListeningExecutorService productQueryExecutor() {
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
processors,
processors * 2,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(256 * processors * 2),
new ThreadFactoryBuilder()
.setNameFormat("ProductQuery-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
return MoreExecutors.listeningDecorator(threadPool);
}
}
监控指标
关键监控点:
- 活跃线程数:当前正在执行任务的线程数
- 队列大小:等待执行的任务数
- 拒绝次数:线程池满了被拒绝的任务数
- 平均执行时间:任务的平均耗时
告警阈值:
- 队列堆积 > 最大线程数 × 100:可能有慢查询
- 拒绝次数突增:流量超过处理能力
- 活跃线程数长期 = 最大线程数:需要扩容
九、何时使用并发
适合的场景
- 多个独立的 IO 操作:查询多个数据表、调用多个外部 API
- 操作之间无依赖:查商品和查评论没有先后顺序
- 对响应时间敏感:用户等待的接口
不适合的场景
- 有依赖关系的操作 :第二步依赖第一步的结果(应该用
transformAsync) - 需要事务一致性:扣库存和创建订单必须在同一事务
- 纯 CPU 密集计算 :应该用
ParallelStream或ForkJoinPool - 需要消息可靠性保证:用 MQ(RabbitMQ、RocketMQ)
十、总结
核心收获
-
Guava ListenableFuture 比 CompletableFuture 更清晰
whenAllSucceed语义明确getDone类型安全- 职责分离,代码可读性强
-
Fan-Out / Fan-In 是通用模式
- ES 用于多索引查询
- 业务系统用于多服务聚合
- 底层思想一致
-
线程池配置有理论依据
- 核心线程数 = CPU 核心数
- 最大线程数 = CPU 核心数 × 2(IO 密集型)
- 队列大小 = 256 × 最大线程数
实践建议
- 从简单场景开始:先用在商品详情这类典型场景
- 加强监控:关注线程池状态和任务执行时间
- 做好降级:考虑异常和超时情况
- 压测验证:用真实流量验证配置合理性
这套模式可以作为你以后所有"多服务并行查询 + 聚合返回"场景的模板:代码清晰、思路简单、足够贴近真实业务。