如何优雅地实现并发控制并避免性能瓶颈
又一个并发控制的坑,我来填
前几天在做一个批量数据处理的功能时,遇到一个特别棘手的问题。简单说就是同时发了太多请求,直接把服务器给干崩了。这里我踩了个大坑,折腾了大半天才搞定,今天就来聊聊这个事。
事情是这样的:有个需求是要从接口拉取上千条数据,每条数据都需要单独调用API处理。刚开始我就直接上手写了,结果一跑起来服务器那边就开始疯狂报警,说是连接数爆表。后来试了下发现同时发几百个请求确实不太靠谱,得想办法控制一下并发量。
先说解决方案,代码就这几行
最后用的是Promise队列的方式,写了一个简单的并发控制函数。代码如下:
function concurrencyControl(tasks, max = 5) {
let running = 0;
let completed = 0;
const results = [];
return new Promise((resolve, reject) => {
const runTask = (index) => {
if (index >= tasks.length) return;
const task = tasks[index];
const currentIndex = index;
running++;
task()
.then(result => {
results[currentIndex] = result;
completed++;
running--;
// 继续执行下一个任务
if (running < max && completed < tasks.length) {
runTask(completed);
}
// 全部完成
if (completed === tasks.length) {
resolve(results);
}
})
.catch(err => {
reject(err);
});
};
// 初始化启动最大并发数的任务
for (let i = 0; i < Math.min(max, tasks.length); i++) {
runTask(i);
}
});
}
使用起来很简单:
const tasks = Array.from({ length: 100 }, (_, i) => () =>
fetch(https://jztheme.com/api/data/${i}).then(res => res.json())
);
concurrencyControl(tasks, 10).then(results => {
console.log('所有任务完成', results);
}).catch(err => {
console.error('出错了', err);
});
为啥会遇到这个问题?说来话长
刚接到这个需求的时候,想着不就是循环调接口嘛,分分钟搞定。结果一运行才发现问题大了:上千个请求一股脑全发出去了。这里有几个关键点:
- 浏览器对同一域名的并发请求数是有限制的(通常是6个左右),但我的场景是Node环境,限制更大
- 服务端扛不住这么大的瞬时压力,尤其是数据库查询这种IO密集型操作
- 即使能扛住,也会导致请求排队严重,整体响应时间反而变长
一开始我想着用setTimeout做个简单的延时,比如每个请求间隔100ms。但这样效率太低了,而且无法充分利用带宽资源。后来又试了下async/await配合for循环,虽然能控制顺序,但还是没法做到真正的并发控制。
关于技术细节的几点思考
在这个方案里有几个地方值得说说:
首先是Promise的状态管理。通过维护running和completed两个计数器,可以精确地控制当前正在执行的任务数。这里我踩了个坑:最初忘记在任务完成后继续调用下一个任务,导致程序卡死。后来加了个递归调用runTask才搞定。
其次是错误处理。最开始我是把catch放在每个task里面,结果发现一旦某个任务报错,整个流程就中断了。改成了在主Promise里统一处理错误,这样即使某个任务失败,其他任务还能继续执行。
第三点是任务结果的顺序保持。因为是并发执行,所以任务完成的顺序可能和开始的顺序不同。通过results数组按索引存储结果,可以确保最终返回的结果顺序和输入的任务顺序一致。
还有个小问题,但无大碍
虽然这个方案基本满足需求了,但还是有个小瑕疵:如果某个任务执行特别慢,可能会导致后面的快任务等待时间变长。理论上可以用更复杂的调度算法来优化,比如优先执行耗时短的任务,但这就太复杂了。
另外,对于特别大规模的任务队列,建议加上进度通知机制。可以在每次任务完成时触发一个回调,方便UI更新或者日志记录。
以上是我踩坑后的总结
总的来说,这个并发控制的方案虽然不是最完美的,但在实际项目中已经够用了。写这篇博客也是希望能让其他小伙伴少走点弯路。如果你有更好的实现方式,或者有其他类似的踩坑经验,欢迎在评论区交流分享。
前端开发就是这样,永远都在踩坑和填坑的路上。共勉!

暂无评论