You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动