从 Guava ListenableFuture 学习生产级并发调用实践

前言

之前从 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);
    }
}

关键点:

  1. executor.submit(Callable) :提交任务到线程池异步执行,立即返回 ListenableFuture
  2. Futures.whenAllSucceed(f1, f2):等待所有 Future 都成功完成,任何一个失败整体就失败
  3. 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
并行目标 多个索引/分片 多个微服务
汇总方式 合并排序 简单组装
核心诉求 正确性 + 性能 响应时间

虽然场景不同,但底层思想一致:

  1. 分解:把大任务拆成多个独立小任务
  2. 并行:利用线程池同时执行
  3. 汇总:等待全部完成后组装结果

六、更多应用场景

订单详情页

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:可能有慢查询
  • 拒绝次数突增:流量超过处理能力
  • 活跃线程数长期 = 最大线程数:需要扩容

九、何时使用并发

适合的场景

  1. 多个独立的 IO 操作:查询多个数据表、调用多个外部 API
  2. 操作之间无依赖:查商品和查评论没有先后顺序
  3. 对响应时间敏感:用户等待的接口

不适合的场景

  1. 有依赖关系的操作 :第二步依赖第一步的结果(应该用 transformAsync
  2. 需要事务一致性:扣库存和创建订单必须在同一事务
  3. 纯 CPU 密集计算 :应该用 ParallelStreamForkJoinPool
  4. 需要消息可靠性保证:用 MQ(RabbitMQ、RocketMQ)

十、总结

核心收获

  1. Guava ListenableFuture 比 CompletableFuture 更清晰

    • whenAllSucceed 语义明确
    • getDone 类型安全
    • 职责分离,代码可读性强
  2. Fan-Out / Fan-In 是通用模式

    • ES 用于多索引查询
    • 业务系统用于多服务聚合
    • 底层思想一致
  3. 线程池配置有理论依据

    • 核心线程数 = CPU 核心数
    • 最大线程数 = CPU 核心数 × 2(IO 密集型)
    • 队列大小 = 256 × 最大线程数

实践建议

  1. 从简单场景开始:先用在商品详情这类典型场景
  2. 加强监控:关注线程池状态和任务执行时间
  3. 做好降级:考虑异常和超时情况
  4. 压测验证:用真实流量验证配置合理性

这套模式可以作为你以后所有"多服务并行查询 + 聚合返回"场景的模板:代码清晰、思路简单、足够贴近真实业务。

相关推荐
Boilermaker19922 小时前
[MySQL] 服务器架构
数据库·mysql·架构
❀͜͡傀儡师2 小时前
SpringBoot 扫码登录全流程:UUID 生成、状态轮询、授权回调详解
java·spring boot·后端
可观测性用观测云2 小时前
观测云在企业应用性能故障分析场景中的最佳实践
后端
a努力。2 小时前
国家电网Java面试被问:Spring Boot Starter 制作原理
java·spring boot·面试
一 乐2 小时前
酒店预约|基于springboot + vue酒店预约系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
LYFlied3 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
我是谁的程序员3 小时前
iOS CPU 占用率在性能问题中的表现形式
后端
一灰灰3 小时前
开发调试与生产分析的利器:MyBatis SQL日志合并插件,让复杂日志秒变可执行SQL
chrome·后端·mybatis