JavaScript中Promise与循环:如何避免请求超限并并行执行
我在处理涉及API请求与Promise的业务场景,目前用axios-rate-limit将请求频率限制为每分钟最多30次:
const requestTiny = axiosRateLimit(axios.create({'baseURL': '*****'}), {maxRequests: 30, perMilliseconds: 60500})
核心函数syncProducts最初仅处理单个商店(BN)的逻辑,代码能正常运行且无请求超限:
export const syncProducts = async (req, res) => { let productsBN = [] let products = await getProductsERP('BN') for (let prod of products) { const product = await getProductERP(prod.store, prod.id) productsBN.push(product) } res.status(201).json(productsBN) }
其中getProductsERP返回产品ID列表,getProductERP通过API获取对应产品详情。
现在需要从多个商店(比如新增ORL)获取数据,且API请求限制按商店划分,并行处理商店能提升效率。但直接复制代码会串行执行,第二个商店需等第一个完成。于是尝试用Promise实现并行:
export const syncProducts = async (req, res) => { let productsORL = [], productsBN = [] Promise.all([ getProductsERP('ORL').then(async products => { productsORL = await Promise.all(products.map(async prod => await getProductERP(prod.store, prod.id))) }), getProductsERP('BN').then(async products => { productsBN = await Promise.all(products.map(async prod => await getProductERP(prod.store, prod.id))) }) ]) .then(res.status(201).json(productsBN.concat(productsORL))) .catch(error => res.status(409).json(error)) }
现在的问题是:getProductsERP执行时限流正常,两个商店能并行处理;但getProductERP执行时会同时发起所有请求,触发Too Many Requests错误。
请问哪里处理有误?如何让getProductERP的Promise遵守API请求限制?
问题根源
你用Promise.all(products.map(...))会一次性把所有getProductERP请求发出去,完全绕过了axios-rate-limit的限流机制——因为限流是针对单个axios实例的请求频率控制,而批量并行请求会瞬间把额度用完。
原来的串行for...of循环能正常限流,是因为每次await getProductERP都会等待上一个请求完成后再发下一个,天然符合“每分钟30次”的节奏。
修复方案
要并行处理商店,但每个商店内部的getProductERP请求要串行或分批限流执行,而不是一次性全部发起。可以用以下两种方式:
方式1:保留串行请求(每个商店内逐个请求),但商店间并行
修改每个商店的处理逻辑,把Promise.all换成串行循环,同时保持商店级别的Promise.all并行:
export const syncProducts = async (req, res) => { try { // 并行处理两个商店 const [productsBN, productsORL] = await Promise.all([ // BN商店:逐个请求产品详情 (async () => { const productIds = await getProductsERP('BN'); const result = []; for (const prod of productIds) { const product = await getProductERP(prod.store, prod.id); result.push(product); } return result; })(), // ORL商店:逐个请求产品详情 (async () => { const productIds = await getProductsERP('ORL'); const result = []; for (const prod of productIds) { const product = await getProductERP(prod.store, prod.id); result.push(product); } return result; })() ]); res.status(201).json([...productsBN, ...productsORL]); } catch (error) { res.status(409).json(error); } }
这种方式和你最初的单个商店逻辑一致,只是把两个商店的处理包装成异步函数,用Promise.all并行执行,既保证了商店间的效率,又让每个商店内的请求遵守限流规则。
方式2:分批并行请求(兼顾效率与限流)
如果想在单个商店内也并行一部分请求(比如同时发5个,而不是逐个),可以自己实现分批处理逻辑,或者用p-limit这类库来控制并发数:
- 先安装
p-limit:
npm install p-limit
- 修改代码:
import pLimit from 'p-limit'; // 限制单个商店内同时发起3个请求(根据限流额度调整) const limit = pLimit(3); export const syncProducts = async (req, res) => { try { const [productsBN, productsORL] = await Promise.all([ (async () => { const productIds = await getProductsERP('BN'); // 用p-limit包装请求,控制并发数 return Promise.all(productIds.map(prod => limit(() => getProductERP(prod.store, prod.id)))); })(), (async () => { const productIds = await getProductsERP('ORL'); return Promise.all(productIds.map(prod => limit(() => getProductERP(prod.store, prod.id)))); })() ]); res.status(201).json([...productsBN, ...productsORL]); } catch (error) { res.status(409).json(error); } }
这种方式可以在不触发限流的前提下,提升单个商店内的请求效率,并发数可以根据你的限流额度(每分钟30次)调整,比如设为5或6,避免瞬间请求过多。
额外注意点
- 确保
getProductERP内部使用的是你配置了限流的requestTiny实例,而不是新创建的axios实例,否则限流规则不会生效。 - 你的原代码中
Promise.all的then方法里直接调用res.status(201).json(...)会导致响应提前发送,因为res.status(...)是同步执行的,正确的做法是在then的回调函数里处理响应,或者用async/await的写法(如上面的解决方案),避免回调地狱和异步问题。
内容的提问来源于stack exchange,提问作者Henrique

