后端接口的 “请求合并与批处理” 进阶:从 “量变” 到 “质变” 的性能飞跃

127 阅读4分钟

在高并发场景中,“频繁小请求” 是常见的性能杀手 —— 例如用户首页需要加载 10 个推荐商品,每个商品单独调用接口,会产生 10 次网络往返、10 次数据库查询。请求合并与批处理技术通过 “批量处理多个同类请求”,将 “N 次小请求” 转为 “1 次大请求”,从根本上减少资源消耗,实现性能的 “质变” 提升。

批处理的核心价值

批处理的核心是 “减少交互次数,提升处理效率”,具体优势:

  • 降低网络开销:N 次请求的网络往返变为 1 次,减少 RTT(往返时间)累积
  • 减少资源竞争:数据库批量查询比 N 次单查更高效(减少锁竞争、IO 次数)
  • 简化客户端逻辑:客户端无需管理并发请求,只需一次调用

批处理的两种典型模式

1. 客户端主动批处理:显式调用批量接口

客户端将多个 ID 或参数合并,主动调用批量接口,适合已知批量需求的场景(如加载多个商品详情)。

接口设计示例

// 批量查询商品接口
@PostMapping("/products/batch-get")
public Result batchGetProducts(@RequestBody BatchProductRequest request) {
    List<Long> productIds = request.getProductIds();
    // 校验批量大小(避免过大)
    if (CollectionUtils.isEmpty(productIds) || productIds.size() > 100) {
        return Result.fail("批量ID数量需在1-100之间");
    }
    // 批量查询(1次数据库查询)
    List<ProductDTO> products = productService.batchQuery(productIds);
    return Result.success(products);
}

// 请求DTO
@Data
public class BatchProductRequest {
    private List<Long> productIds; // 商品ID列表
}

客户端调用

// 批量获取商品详情
async function getProducts(ids) {
  const res = await axios.post('/products/batch-get', { productIds: ids });
  return res.data.data;
}

// 使用示例:获取ID为1,2,3的商品
getProducts([1,2,3]).then(products => {
  // 处理结果
});

2. 服务端透明批处理:自动合并请求

对于无法提前预知的零散请求(如多个并发的单商品查询),服务端通过 “请求合并器” 自动合并短时间内的同类请求,对客户端透明。

实现原理(基于 Guava 的 LoadingCache)

@Service
public class ProductBatchService {
    // 缓存+批量加载器
    private final LoadingCache<Long, ProductDTO> productCache;

    public ProductBatchService(ProductMapper productMapper) {
        this.productCache = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .build(new CacheLoader<Long, ProductDTO>() {
                    // 单条加载(fallback)
                    @Override
                    public ProductDTO load(Long productId) {
                        return productMapper.selectById(productId);
                    }

                    // 批量加载(核心)
                    @Override
                    public Map<Long, ProductDTO> loadAll(Iterable<? extends Long> productIds) {
                        List<Long> ids = Lists.newArrayList(productIds);
                        return productMapper.batchSelectByIds(ids).stream()
                                .collect(Collectors.toMap(ProductDTO::getId, Function.identity()));
                    }
                });
    }

    // 对外提供单条查询接口(内部自动批量处理)
    public ProductDTO getProduct(Long productId) {
        try {
            return productCache.get(productId);
        } catch (ExecutionException e) {
            throw new RuntimeException("获取商品失败", e);
        }
    }
}

工作流程

  1. 多个线程同时调用getProduct(1)getProduct(2)getProduct(3)
  2. Guava Cache 检测到多个未缓存的 ID,自动合并为批量查询loadAll([1,2,3])
  3. 批量查询结果缓存后,分别返回给各线程
  4. 后续查询相同 ID 时直接从缓存获取

3. 两种模式对比

模式优势劣势适用场景
客户端主动批处理可控性高,适合已知批量需求客户端需修改逻辑列表加载、批量操作
服务端透明批处理对客户端透明,无需修改实现复杂,有延迟风险零散、高频的单条查询

批处理的关键设计要点

1. 批量大小控制:防 “过大请求”

  • 单次批量请求的 ID 数量需设上限(如 100 个),避免:

    • 单个请求处理时间过长导致超时

    • 数据库批量操作锁表时间过长

    • 内存占用过大(如批量返回 1000 条大对象)

处理超过上限的请求

// 超过上限时分片处理
public List<ProductDTO> batchQueryLarge(List<Long> productIds) {
    List<ProductDTO> result = new ArrayList<>();
    // 按100个ID分片
    List<List<Long>> partitions = Lists.partition(productIds, 100);
    for (List<Long> partition : partitions) {
        result.addAll(productMapper.batchSelectByIds(partition));
    }
    return result;
}

2. 超时与重试策略

  • 批量接口超时时间需适当延长(如单条接口超时 1 秒,批量接口可设 3 秒)
  • 重试需谨慎:批量请求失败时,建议只重试失败的 ID(避免重复处理成功部分)

3. 一致性保证

  • 批量查询结果需与单条查询一致(如过滤条件、权限校验)
  • 批量更新 / 删除需考虑事务一致性(如 “要么全成功,要么全失败” 或 “部分失败时补偿”)

避坑指南

  • 不要过度批处理:低频请求或单次请求数据量小的场景,批处理收益有限

  • 避免 “批处理依赖”:批量接口不应依赖其他批量接口,防止级联失败

  • 监控批处理效果:统计批量接口的平均处理时间、压缩率(N 次单查耗时 / 1 次批量耗时),确保优化有效

批处理不是 “银弹”,但它是高并发场景下的 “性能利器”。合理使用批处理技术,能将接口性能提升数倍甚至数十倍,这正是 “减少次数比优化单次” 更有效的后端性能优化哲学。