在高并发场景中,“频繁小请求” 是常见的性能杀手 —— 例如用户首页需要加载 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);
}
}
}
工作流程:
- 多个线程同时调用
getProduct(1)、getProduct(2)、getProduct(3) - Guava Cache 检测到多个未缓存的 ID,自动合并为批量查询
loadAll([1,2,3]) - 批量查询结果缓存后,分别返回给各线程
- 后续查询相同 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 次批量耗时),确保优化有效
批处理不是 “银弹”,但它是高并发场景下的 “性能利器”。合理使用批处理技术,能将接口性能提升数倍甚至数十倍,这正是 “减少次数比优化单次” 更有效的后端性能优化哲学。