async-await搭配.map时sleep函数无效的问题排查
核心原因:并发模式的差异
map+Promise.all是「无限制瞬时并发」
map会一次性遍历完所有300+条艺人数据,立即创建并执行所有请求的Promise,相当于瞬间把几百个请求同时发给Spotify API。这时候哪怕单个请求触发429后进入catch执行sleep,其他请求已经处于发送/等待状态,sleep只能延迟当前请求的重试,无法阻止已经发出的其他请求,自然会持续触发限流,而且整个Promise.all的等待过程不会被单个sleep打断,看起来就是“sleep未生效,循环继续执行”。for循环(带await)是「串行/可控并发」
用for...of循环结合await时,每次只会处理一个请求:前一个请求完成(包括重试逻辑)后,才会进入下一次循环发送下一个请求。遇到429时,await sleep会暂停整个循环的执行,等Retry-After时长结束后再重试当前请求,之后才继续处理下一条数据。这种逐个发送的方式不会瞬间打满Spotify的请求配额,所以不容易触发限流,sleep的重试逻辑也能正常生效。
代码逻辑对比
问题复现的map+Promise.all写法
const fetchSpotifyData = async (artistId) => { // 调用Spotify API的具体逻辑 }; const promises = artists.map(async (artist) => { try { return await fetchSpotifyData(artist.id); } catch (err) { if (err.status === 429) { const retryAfter = parseInt(err.headers.get('Retry-After'), 10); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); return await fetchSpotifyData(artist.id); } throw err; } }); await Promise.all(promises);
map会一次性生成所有Promise,所有请求几乎同时发出,限流触发后,单个请求的sleep无法阻止其他请求,导致重试请求和其他未完成请求继续挤占配额,限流无法缓解。
正常工作的for循环写法
const fetchSpotifyData = async (artistId) => { // 调用Spotify API的具体逻辑 }; for (const artist of artists) { try { await fetchSpotifyData(artist.id); } catch (err) { if (err.status === 429) { const retryAfter = parseInt(err.headers.get('Retry-After'), 10); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); await fetchSpotifyData(artist.id); } throw err; } }
循环会等待当前请求(包括重试)完成后再处理下一个,请求发送速率被控制,不会触发瞬时限流,sleep也能正常暂停循环等待重试。
扩展:兼顾并发与限流的方案
如果不想完全串行损失效率,可以手动实现批量请求(比如每次处理10条,等批量完成后再处理下一批),或使用p-limit这类库控制同时发送的请求数量,既保证处理速度,又不触发API限流规则。
内容的提问来源于stack exchange,提问作者smoothlikebutter

