如何优雅地实现并发控制并避免性能瓶颈

上官国玲 前端 阅读 999
赞 32 收藏
二维码
手机扫码查看
反馈

又一个并发控制的坑,我来填

前几天在做一个批量数据处理的功能时,遇到一个特别棘手的问题。简单说就是同时发了太多请求,直接把服务器给干崩了。这里我踩了个大坑,折腾了大半天才搞定,今天就来聊聊这个事。

如何优雅地实现并发控制并避免性能瓶颈

事情是这样的:有个需求是要从接口拉取上千条数据,每条数据都需要单独调用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更新或者日志记录。

以上是我踩坑后的总结

总的来说,这个并发控制的方案虽然不是最完美的,但在实际项目中已经够用了。写这篇博客也是希望能让其他小伙伴少走点弯路。如果你有更好的实现方式,或者有其他类似的踩坑经验,欢迎在评论区交流分享。

前端开发就是这样,永远都在踩坑和填坑的路上。共勉!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论