Promise并发先限流
很多技术问题看起来是某个 API 用错了,实际更像一次边界没有提前说清的连锁反应。一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开,这种情况并不稀奇:功能表面能跑,真正进入复杂路径后,隐藏假设才开始一个个冒出来。
这篇文章想讨论的不是把Promise并发池讲成一套万能口诀,而是把它放回真实工作里看:哪些规则需要提前定,哪些复杂度可以延后,哪些地方一旦偷懒就会变成排查成本。我的判断是,先把边界收住,再谈抽象、性能或体验,通常更稳。
Promise.all不是批处理方案
在一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开这个场景里,它适合少量相互独立的任务,不适合把几百个请求瞬间丢出去。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。Promise并发池如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
从机制上看,任务队列、并发窗口、失败收集、重试和取消不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么全量并发完成最快,但也最容易把压力一次性打给下游。
落地时建议先做一件小事:先估算下游能接受的并发窗口。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:快不等于稳,尤其在管理后台里更明显。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
判断这部分做得好不好,不要只看功能是否跑通,而要看批量任务在限定并发下可取消、可重试、可展示部分失败。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“Promise.all不是批处理方案”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
图里只保留了和Promise并发池直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
并发窗口要能调整
在一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开这个场景里,不同接口、不同网络和不同账号权限下,合适并发数不一样。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。Promise并发池如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“并发窗口要能调整”这个小节里看,相关机制并不是背景知识,任务队列、并发窗口、失败收集、重试和取消不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么全量并发完成最快,但也最容易把压力一次性打给下游。
落地时建议先做一件小事:把 limit 做成配置或按场景选择。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:写死 20 看似简单,线上遇到慢接口就难调。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“并发窗口要能调整”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看批量任务在限定并发下可取消、可重试、可展示部分失败。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“并发窗口要能调整”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
针对“并发窗口要能调整”,可以把检查动作落成三项:
先写清本场景里的关键对象:Promise并发池。
再标出会影响它的机制:任务队列、并发窗口、失败收集、重试和取消。
最后补上失败时的判断标准:批量任务在限定并发下可取消、可重试、可展示部分失败。
失败结果要完整保留
在一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开这个场景里,批量任务最怕第一个失败直接中断,用户不知道哪些成功哪些失败。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。Promise并发池如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“失败结果要完整保留”这个小节里看,相关机制并不是背景知识,任务队列、并发窗口、失败收集、重试和取消不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么全量并发完成最快,但也最容易把压力一次性打给下游。
落地时建议先做一件小事:用 allSettled 或自定义结果结构收集状态。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:只返回失败,后续补偿和重试都会缺信息。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“失败结果要完整保留”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看批量任务在限定并发下可取消、可重试、可展示部分失败。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“失败结果要完整保留”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
换到“失败结果要完整保留”这一步,图里只保留了和Promise并发池直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
下面这段代码只表达思路,重点不在复制,而在看清边界放在哪里:
- async function runLimit(tasks, limit) {
- const ret = []
- const workers = Array.from({ length: limit }, async () => {
- while (tasks.length) ret.push(await tasks.shift()())
- })
- await Promise.all(workers)
- return ret
- }
重试要限制次数和条件
在一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开这个场景里,网络抖动可以重试,参数错误和权限错误不该重试。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。Promise并发池如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“重试要限制次数和条件”这个小节里看,相关机制并不是背景知识,任务队列、并发窗口、失败收集、重试和取消不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么全量并发完成最快,但也最容易把压力一次性打给下游。
落地时建议先做一件小事:按错误类型决定是否重试。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:无差别重试会把错误请求放大。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“重试要限制次数和条件”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看批量任务在限定并发下可取消、可重试、可展示部分失败。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“重试要限制次数和条件”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
针对“重试要限制次数和条件”,可以把检查动作落成三项:
先写清本场景里的关键对象:Promise并发池。
在“重试要限制次数和条件”里标出会影响它的机制:任务队列、并发窗口、失败收集、重试和取消。
为“重试要限制次数和条件”补上失败时的判断标准:批量任务在限定并发下可取消、可重试、可展示部分失败。
换到“重试要限制次数和条件”这一步,图里只保留了和Promise并发池直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
交互上要允许暂停和取消
在一个后台页面选择 200 条记录后批量同步,前端直接 Promise.all 请求接口,浏览器、网关和后端日志同时炸开这个场景里,用户发现选错批次时,应该能停止未开始的任务。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。Promise并发池如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“交互上要允许暂停和取消”这个小节里看,相关机制并不是背景知识,任务队列、并发窗口、失败收集、重试和取消不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么全量并发完成最快,但也最容易把压力一次性打给下游。
落地时建议先做一件小事:任务队列要区分已开始和待开始。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:取消后仍继续请求,会让用户失去控制感。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“交互上要允许暂停和取消”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看批量任务在限定并发下可取消、可重试、可展示部分失败。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“交互上要允许暂停和取消”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
收尾时看这三个信号
第一,看问题能不能被命名。比如这篇里的核心不是泛泛的“优化一下”,而是Promise并发池有没有清楚边界。能命名的问题,才容易进入评审、测试和复盘。
第二,看失败能不能被复现。围绕批量任务在限定并发下可取消、可重试、可展示部分失败设计一组小样本,比等线上偶发问题更可靠。样本不需要复杂,但要覆盖正常、异常、边界和恢复。
第三,看团队能不能做出一致选择。全量并发完成最快,但也最容易把压力一次性打给下游,这类取舍没有绝对答案,但必须有理由、有记录、有回滚空间。否则今天靠经验放过的点,明天就会变成另一个人看不懂的坑。
真正有价值的工程文章,不是把每个概念都讲满,而是帮读者在下次遇到类似场景时更早地停一下:这件事的边界定了吗,失败路径想过了吗,验收标准能说清吗。只要这三个问题能回答,很多复杂度就已经少了一半。
