Express限制重负载API单次执行:浏览器重试触发误限制问题
解决Express API重负载任务的并发限制问题
这个问题我之前也碰到过!express-rate-limit其实不太适合你这种「限制同一任务同时运行」的场景——它本质是按时间窗口统计请求次数,而你需要的是一个「运行状态锁」,来确保同一时间只有一个重负载任务在执行。
为什么会出现2分钟后触发限制?
你猜的没错,很多浏览器或HTTP客户端(尤其是GET请求)会在请求超时后自动重试,默认超时时间经常是2分钟左右。当你的第一个请求还在后台处理时,浏览器发起的重试请求会被express-rate-limit算作第二次请求,直接触发max:1的限制规则。
正确的解决方案:用「运行锁」替代速率限制
我们需要维护一个标记,记录当前是否有该API的任务在运行,而不是统计时间窗口内的请求数。
单实例部署方案(适合单服务器)
在你的路由控制器里添加一个全局锁变量,请求开始时标记为运行中,结束后释放锁:
// BillingController所在文件 let isBillingJobRunning = false; router.get('/billableElements', async (request, response) => { // 检查是否有正在运行的任务 if (isBillingJobRunning) { logger.log('Rejected: billing job is already running'); return response.status(429).send('There is already a running execution of the request. You must wait for it to be finished before starting a new one.'); } logger.log('Route [billableElements] called'); const { startDate, endDate } = request.query; try { isBillingJobRunning = true; // 标记任务开始 const configDoc = await metadataBucket.getAsync(process.env.BILLING_CONFIG_FILE || 'CONFIG_BILLING'); const billableElements = await getBillableElements(startDate, endDate, configDoc.value); const csv = await produceCSV(billableElements); logger.log('csv produced'); response.status(200).send(`${csv}`); } catch (err) { logger.error('An error occurred while getting billable elements.', err); response.status(500).send('An internal error occurred.'); } finally { isBillingJobRunning = false; // 不管成功失败,都释放锁 } });
多实例部署方案(适合集群/分布式环境)
如果你的Express应用是多实例部署,全局变量无法跨实例共享,需要用Redis等分布式锁:
// 先安装redis包:npm install redis const redis = require('redis'); const client = redis.createClient({ // 你的Redis连接配置,比如host、port等 }); router.get('/billableElements', async (request, response) => { const lockKey = 'billing:job:running'; // 尝试获取锁,设置35分钟过期(比最长任务时间30分钟长一点,防止任务崩溃后锁无法释放) const lockAcquired = await client.set(lockKey, 'active', { NX: true, EX: 35 * 60 }); if (!lockAcquired) { logger.log('Rejected: billing job is already running'); return response.status(429).send('There is already a running execution of the request. You must wait for it to be finished before starting a new one.'); } logger.log('Route [billableElements] called'); const { startDate, endDate } = request.query; try { const configDoc = await metadataBucket.getAsync(process.env.BILLING_CONFIG_FILE || 'CONFIG_BILLING'); const billableElements = await getBillableElements(startDate, endDate, configDoc.value); const csv = await produceCSV(billableElements); logger.log('csv produced'); response.status(200).send(`${csv}`); } catch (err) { logger.error('An error occurred while getting billable elements.', err); response.status(500).send('An internal error occurred.'); } finally { await client.del(lockKey); // 任务结束后释放锁 } });
额外优化:阻止客户端自动重试
虽然锁机制已经能解决问题,但你也可以通过响应头提示客户端不要重试:
// 在路由里添加响应头 response.setHeader('Retry-After', 30 * 60); // 告诉客户端30分钟后再试 response.setHeader('Cache-Control', 'no-store'); // 禁止缓存请求结果
内容的提问来源于stack exchange,提问作者MHogge




