前言

试算接口是用户进入商品页时最先触发的后端链路。如果它慢,用户看到的就是价格迟迟不出来、拼团按钮一直加载。这个接口里至少要查活动配置、商品信息、标签可见性,后面还可能查库存和动态配置。串行做当然简单,但会把多个 IO 等待累加起来。

我的处理方式是在 MarketNode 里使用 FutureTask + 线程池并行加载活动配置和 SKU 信息。这样两个互不依赖的查询可以同时进行,接口耗时接近两者中的最大值,而不是两者之和。

试算接口为什么慢

商品信息和活动配置属于不同数据源或不同表,真实生产里商品可能来自商品中心 RPC,活动来自营销库,标签来自 Redis。它们之间没有强依赖,只有最终计算优惠时需要同时拿到。这样的场景很适合做前置并行加载。

当然,并行不是免费午餐。线程池大小、队列长度、超时时间和拒绝策略都要配置好,否则高峰期可能把自己打满。

线程池配置

thread:
  pool:
    executor:
      config:
        core-pool-size: 20
        max-pool-size: 50
        keep-alive-time: 5000
        block-queue-size: 5000
        policy: CallerRunsPolicy

CallerRunsPolicy 的含义是队列满时由调用线程执行任务。它不会直接丢任务,但会反向拖慢入口线程,形成一种温和的背压。对于试算接口,我更愿意让请求变慢一点,也不希望静默丢掉活动或商品查询。

FutureTask并行加载

QueryGroupBuyActivityDiscountVOThreadTask activityTask =
        new QueryGroupBuyActivityDiscountVOThreadTask(
                requestParameter.getSource(),
                requestParameter.getChannel(),
                requestParameter.getGoodsId(),
                repository);
FutureTask<GroupBuyActivityDiscountVO> activityFuture = new FutureTask<>(activityTask);
threadPoolExecutor.execute(activityFuture);

QuerySkuVOFromDBThreadTask skuTask =
        new QuerySkuVOFromDBThreadTask(requestParameter.getGoodsId(), repository);
FutureTask<SkuVO> skuFuture = new FutureTask<>(skuTask);
threadPoolExecutor.execute(skuFuture);

dynamicContext.setGroupBuyActivityDiscountVO(activityFuture.get(timeout, TimeUnit.MINUTES));
dynamicContext.setSkuVO(skuFuture.get(timeout, TimeUnit.MINUTES));

这段代码的意图很明确:并行查活动和 SKU,然后写入动态上下文。为什么不用 CompletableFuture?也可以用。这里选 FutureTask 是因为链路简单,只需要提交任务、等待结果、设置超时。等后续节点更多、需要组合编排时,CompletableFuture 的 thenCombine 会更自然。

超时和降级

并发查询一定要配超时。没有超时的 Future.get 会把请求线程卡死,最终把 Tomcat 线程池拖住。当前代码用 `get(timeout, TimeUnit.MINUTES)`,实际生产我会把单位调到毫秒或秒级,并根据节点重要性做降级。

try {
    GroupBuyActivityDiscountVO activity = activityFuture.get(300, TimeUnit.MILLISECONDS);
    SkuVO sku = skuFuture.get(300, TimeUnit.MILLISECONDS);
    context.setGroupBuyActivityDiscountVO(activity);
    context.setSkuVO(sku);
} catch (TimeoutException e) {
    activityFuture.cancel(true);
    skuFuture.cancel(true);
    context.setEnable(false);
    log.warn("trial query timeout, goodsId={}", request.getGoodsId(), e);
}

对于下午茶商品页来说,营销试算失败时可以返回原价和不可参团,不应该让整个页面不可用。这个降级策略和简历里写的“降低接口响应时间”是一体两面:一方面做并行加载,一方面给慢依赖设置边界。

小结

FutureTask 不是为了展示并发 API,而是为了解决试算接口的真实延迟问题。活动、商品、标签这些数据可以并行加载,但线程池、超时、取消和降级必须一起设计。否则并发只是把串行慢改成并发乱。