1. 为什么我们需要接口请求合并
第一次意识到接口请求合并的重要性,是在去年的一次大促压测中。当时我们的订单查询接口QPS突然飙升到平时的10倍,数据库连接池很快被耗尽,整个系统濒临崩溃。紧急排查后发现,前端页面在渲染订单列表时,竟然为每个订单单独调用了获取物流状态的接口——这意味着用户查看10个订单就会产生10次接口调用。
这种"一个订单一次请求"的模式在用户量激增时简直就是性能杀手。后来我们通过合并物流状态查询请求,将10次调用压缩为1次,数据库负载直接下降了90%。这个案例让我深刻体会到:在高并发场景下,减少不必要的网络IO往往比升级硬件更有效。
接口请求合并的核心思想很简单:将多个独立请求合并为一个批量请求,减少网络传输开销和服务端处理压力。但实际操作中需要考虑很多细节,比如合并策略、超时控制、结果拆分等。下面我会结合具体案例,分享几种实用的实现方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 请求合并的典型应用场景
2.1 电商平台的商品详情页
打开一个电商APP的商品页,你可能需要同时获取:
- 商品基础信息
- 库存状态
- 价格促销
- 用户评价
- 推荐商品
如果每个模块都独立调用接口,假设每个接口耗时50ms,串行调用就需要250ms。而通过合并接口,可以做到:
java复制// 伪代码示例:合并请求体
{
"productId": "123",
"requireFields": ["baseInfo","stock","price","reviews","recommend"]
}
服务端一次查询所有数据,响应时间可以控制在80ms以内。
2.2 社交媒体的动态信息流
微博、朋友圈这类场景中,每条动态可能需要获取:
- 用户资料
- 正文内容
- 点赞状态
- 评论摘要
- 相关广告
采用传统的分步加载会导致明显的"瀑布流"效果。某社交APP的实测数据显示,使用接口合并后,信息流首屏渲染时间从1.2秒降低到600毫秒。
2.3 后台管理系统的表格数据
管理系统常需要同时展示:
- 分页数据
- 汇总统计
- 筛选条件
- 权限控制
曾经处理过一个ERP系统的性能问题:前端分页表格每次翻页会触发4个独立API调用。通过设计/table/query合并接口,TPS从120提升到350。
3. 实现请求合并的四种技术方案
3.1 前端主动合并方案
适用于:前端明确知道需要哪些数据的场景
实现方式:
javascript复制// 前端封装合并请求
async function fetchCombinedData(params) {
const requests = [
service.getProductBase(params.productId),
service.getProductStock(params.productId),
service.getProductPrice(params.productId)
];
return Promise.all(requests);
}
// 使用示例
const [baseInfo, stock, price] = await fetchCombinedData({
productId: '123'
});
优点:
- 实现简单直接
- 不需要服务端改造
缺点:
- 仍会产生多个网络请求
- 服务端压力没有真正减轻
3.2 网关层合并方案
适用于:微服务架构下的接口聚合
Nginx配置示例:
nginx复制location /api/combined {
# 并行调用多个后端接口
mirror /api/product/base;
mirror /api/product/stock;
mirror /api/product/price;
}
Spring Cloud Gateway实现:
java复制@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("combined_route", r -> r.path("/api/combined")
.filters(f -> f.prefixPath("/api")
.modifyRequestBody(String.class, String.class,
(exchange, body) -> {
// 请求体转换逻辑
return Mono.just(transformedBody);
}))
.uri("lb://product-service"))
.build();
}
优点:
- 对业务代码无侵入
- 可以复用现有接口
缺点:
- 网关可能成为性能瓶颈
- 错误处理较复杂
3.3 服务端批量接口方案
适用于:新建系统的接口设计
GraphQL实现示例:
graphql复制query {
product(id: "123") {
name
price
stock {
warehouse
quantity
}
reviews {
content
rating
}
}
}
RESTful批量接口设计:
java复制@PostMapping("/products/batch")
public BatchProductResponse getBatchProducts(
@RequestBody BatchProductRequest request) {
List<Product> products = productService.batchQuery(request.getIds());
Map<String, Integer> stocks = stockService.batchQuery(request.getIds());
return new BatchProductResponse(products, stocks);
}
优点:
- 网络开销最小化
- 服务端可以优化查询
缺点:
- 需要设计新的接口规范
- 客户端需要适配新接口
3.4 请求缓存合并方案
适用于:读多写少的场景
使用Hystrix实现请求折叠:
java复制@HystrixCommand(fallbackMethod = "getStockFallback",
commandProperties = {
@HystrixProperty(name="requestCache.enabled",value="true"),
@HystrixProperty(name="collapser.enabled",value="true"),
@HystrixProperty(name="collapser.maxRequestsInBatch",value="100"),
@HystrixProperty(name="collapser.timerDelayInMilliseconds",value="20")
})
public Future<Stock> getStockAsync(String productId) {
return new AsyncResult<Stock>() {
@Override
public Stock invoke() {
return stockService.getStock(productId);
}
};
}
Guava Cache实现:
java复制LoadingCache<String, Product> productCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, Product>() {
@Override
public Product load(String key) {
return productService.getProduct(key);
}
@Override
public Map<String, Product> loadAll(Iterable<? extends String> keys) {
return productService.batchGetProducts(Lists.newArrayList(keys));
}
});
优点:
- 自动合并窗口期内的请求
- 减少重复计算
缺点:
- 需要合理设置时间窗口
- 不适合实时性要求高的场景
4. 请求合并的实践要点与避坑指南
4.1 合并粒度的权衡
常见误区:
- 过度合并:把不相关的接口强行合并,导致接口职责不清
- 合并不足:该合并的没合并,性能提升有限
合理做法:
- 分析调用链路,找出高频组合
- 按业务语义划分合并边界
- 控制单个合并接口的响应体积
4.2 超时与重试策略
典型问题场景:
- 合并接口中部分成功部分失败
- 某个子请求超时影响整体
解决方案:
java复制// 为每个子请求设置独立超时
List<CompletableFuture<Result>> futures = requests.stream()
.map(req -> CompletableFuture.supplyAsync(
() -> callService(req),
CompletableFuture.delayedExecutor(timeout, TimeUnit.MILLISECONDS)
))
.collect(Collectors.toList());
// 收集结果时处理部分失败
Map<String, Result> results = new HashMap<>();
for (int i = 0; i < futures.size(); i++) {
try {
results.put(requests.get(i).getId(), futures.get(i).get());
} catch (Exception e) {
results.put(requests.get(i).getId(), new ErrorResult(e));
}
}
4.3 结果映射与拆解
复杂合并接口的响应设计:
json复制{
"status": "PARTIAL_SUCCESS",
"data": {
"product": {
"123": {
"base": {...},
"stock": {...}
},
"456": {
"error": "NOT_FOUND"
}
},
"recommend": [...]
}
}
前端处理示例:
javascript复制function handleResponse(response) {
if (response.status === 'SUCCESS') {
// 正常处理
} else if (response.status === 'PARTIAL_SUCCESS') {
// 部分成功处理
Object.entries(response.data.product).forEach(([id, result]) => {
if (result.error) {
showError(id, result.error);
} else {
renderProduct(id, result);
}
});
}
}
4.4 监控与降级
关键监控指标:
- 合并请求的平均批次数
- 合并节省的请求比例
- 合并接口的响应时间分布
降级策略示例:
java复制// 根据系统负载动态调整合并窗口
public int getCurrentBatchWindow() {
double load = getSystemLoad();
if (load > 0.8) {
return 50; // 增大窗口应对高负载
} else {
return 20; // 默认窗口
}
}
5. 性能优化效果实测对比
在某内容平台的实践数据:
| 指标 | 合并前 | 合并后 | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 320ms | 180ms | 43.75% |
| 数据库QPS | 4500 | 1200 | 73.33% |
| 网络带宽消耗 | 12MB/s | 4MB/s | 66.67% |
| 错误率 | 1.2% | 0.3% | 75% |
测试环境对比(JMeter压测结果):

关键发现:
- 合并效果与请求相似度正相关
- 最佳合并窗口在10-50ms之间
- 批量接口比并行请求效率高30%
6. 不同技术栈的实现示例
6.1 Node.js实现
使用async_hooks实现请求合并:
javascript复制const asyncHooks = require('async_hooks');
const batcher = new BatchProcessor({
maxBatchSize: 20,
timeout: 15
});
// 拦截所有数据库查询
async function queryProduct(id) {
return batcher.add(id);
}
class BatchProcessor {
constructor(options) {
this.queue = [];
this.timer = null;
this.maxBatchSize = options.maxBatchSize;
this.timeout = options.timeout;
}
add(id) {
return new Promise((resolve) => {
this.queue.push({ id, resolve });
if (this.queue.length >= this.maxBatchSize) {
this.processQueue();
} else if (!this.timer) {
this.timer = setTimeout(
() => this.processQueue(),
this.timeout
);
}
});
}
async processQueue() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const items = this.queue.splice(0, this.maxBatchSize);
const ids = items.map(item => item.id);
try {
const results = await db.batchQuery(ids);
items.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
items.forEach(item => {
item.resolve({ error });
});
}
}
}
6.2 Go语言实现
使用channel实现请求合并:
go复制type BatchRequest struct {
Key string
Response chan interface{}
}
type Batcher struct {
requests chan BatchRequest
timeout time.Duration
size int
}
func NewBatcher(size int, timeout time.Duration) *Batcher {
b := &Batcher{
requests: make(chan BatchRequest),
timeout: timeout,
size: size,
}
go b.process()
return b
}
func (b *Batcher) Do(key string) interface{} {
req := BatchRequest{
Key: key,
Response: make(chan interface{}, 1),
}
b.requests <- req
return <-req.Response
}
func (b *Batcher) process() {
var batch []BatchRequest
timer := time.NewTimer(b.timeout)
for {
select {
case req := <-b.requests:
batch = append(batch, req)
if len(batch) >= b.size {
b.execute(batch)
batch = nil
timer.Reset(b.timeout)
}
case <-timer.C:
if len(batch) > 0 {
b.execute(batch)
batch = nil
}
timer.Reset(b.timeout)
}
}
}
func (b *Batcher) execute(batch []BatchRequest) {
keys := make([]string, len(batch))
for i, req := range batch {
keys[i] = req.Key
}
// 批量查询
results, err := db.BatchGet(keys)
if err != nil {
for _, req := range batch {
req.Response <- err
}
return
}
for i, req := range batch {
req.Response <- results[i]
}
}
6.3 Python实现
使用asyncio实现请求合并:
python复制import asyncio
from collections import defaultdict
class Batcher:
def __init__(self, batch_size=10, timeout=0.05):
self.batch_size = batch_size
self.timeout = timeout
self.queue = []
self.loop = asyncio.get_event_loop()
self.current_batch = None
async def add(self, key):
if not self.current_batch:
self.current_batch = Batch(self)
self.loop.call_later(
self.timeout,
self.current_batch.process
)
future = asyncio.Future()
self.current_batch.add(key, future)
if len(self.current_batch) >= self.batch_size:
self.current_batch.process()
self.current_batch = None
return await future
class Batch:
def __init__(self, batcher):
self.batcher = batcher
self.items = []
self.processed = False
def __len__(self):
return len(self.items)
def add(self, key, future):
self.items.append((key, future))
def process(self):
if self.processed:
return
self.processed = True
keys = [item[0] for item in self.items]
async def _process():
try:
results = await db.batch_get(keys)
for (key, future), result in zip(self.items, results):
future.set_result(result)
except Exception as e:
for _, future in self.items:
future.set_exception(e)
asyncio.create_task(_process())
7. 进阶优化技巧
7.1 动态合并策略
根据系统负载自动调整:
java复制public class DynamicBatcher {
private final int maxBatchSize;
private final long maxTimeoutMs;
private final double loadFactorThreshold;
private volatile int currentBatchSize;
private volatile long currentTimeout;
public DynamicBatcher(int maxBatchSize, long maxTimeoutMs,
double loadThreshold) {
this.maxBatchSize = maxBatchSize;
this.maxTimeoutMs = maxTimeoutMs;
this.loadFactorThreshold = loadThreshold;
resetToDefaults();
}
public void adjustParameters() {
double load = SystemLoadCalculator.getLoad();
if (load > loadFactorThreshold) {
// 高负载时增大批次减少请求数
currentBatchSize = Math.min(
maxBatchSize,
currentBatchSize + 5
);
currentTimeout = Math.min(
maxTimeoutMs,
currentTimeout + 10
);
} else {
// 低负载时减小批次降低延迟
currentBatchSize = Math.max(
1,
currentBatchSize - 2
);
currentTimeout = Math.max(
10,
currentTimeout - 5
);
}
}
public int getCurrentBatchSize() {
return currentBatchSize;
}
public long getCurrentTimeout() {
return currentTimeout;
}
private void resetToDefaults() {
currentBatchSize = Math.max(1, maxBatchSize / 2);
currentTimeout = maxTimeoutMs / 2;
}
}
7.2 分层合并架构
大型系统中的分层设计:
code复制客户端层
│
▼
API网关层(粗粒度合并)
│
▼
业务服务层(细粒度合并)
│
▼
数据访问层(批量操作)
7.3 智能预合并
基于历史数据的预测合并:
python复制class PredictiveBatcher:
def __init__(self):
self.request_patterns = defaultdict(int)
self.model = self.train_model()
def record_request(self, api, params):
key = self._generate_pattern_key(api, params)
self.request_patterns[key] += 1
def predict_next_requests(self, current_api):
# 使用简单马尔可夫模型预测
possible_next = []
for pattern in self.request_patterns:
if pattern.startswith(current_api):
next_api = pattern.split('->')[1]
possible_next.append((next_api, self.request_patterns[pattern]))
return sorted(possible_next, key=lambda x: -x[1])[:3]
def train_model(self):
# 实际项目可以用更复杂的模型
return SimpleMarkovModel()
7.4 跨服务合并
微服务场景下的合并策略:
java复制@HystrixCommand(fallbackMethod = "getCombinedDataFallback")
public CombinedData getCombinedData(String userId) {
return new CombinedData(
userService.getUser(userId),
orderService.getRecentOrders(userId),
paymentService.getPaymentMethods(userId)
);
}
// 使用缓存结果避免重复计算
@Cacheable(value = "combinedData", key = "#userId")
public CombinedData getCombinedDataWithCache(String userId) {
return getCombinedData(userId);
}
在实际项目中,接口请求合并带来的性能提升往往超出预期。曾有一个电商项目通过系统化的请求合并改造,在双十一期间用同样的服务器资源支撑了3倍的流量。关键是要根据具体业务场景选择合适的合并策略,并做好监控和调优。
