← 中文资源

简体中文版本

Express Redis 分布式限流

用 express-rate-limit 配 Redis 存储实现跨实例限流:按 API key 或用户身份取键,健康检查跳过,标准限流响应头回传。

阅读英文原文

示例说明

多实例部署下内存计数器各算各的,总额度被放大成实例数倍。Redis 存储把窗口计数集中到一处,所有实例共享同一把尺子。keyGenerator 决定限流主体:优先 API key,其次用户 ID,最后回退 IP——比纯 IP 键更公平,避免共享出口的公司用户互相挤压。

技术要点

  • Redis 存储让限流口径全局一致
  • keyGenerator 优先业务身份而非裸 IP
  • standardHeaders 输出标准限流响应头

代码解读

store 适配器把 sendCommand 委托给 redis 客户端;窗口 60 秒 120 次。skip 让健康检查端点不消耗配额,避免编排器探活误伤。超限处理器返回结构化错误并透出 Retry-After,合规客户端可据此退避。

使用建议

API key 泄露时单键限流会放大影响,配合键级吊销机制。Redis 故障时的降级策略要预先决定:拒绝或放行。限流维度按业务分级:公开端点严、内部回源宽。

代码示例

import express from 'express';
import rateLimit from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379' });
await redis.connect();

const limiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  windowMs: 60_000,
  limit: 120,
  keyGenerator: (req) => {
    const apiKey = req.header('x-api-key');
    const subject = apiKey ?? req.header('x-user-id') ?? req.ip;
    return subject + ':' + (apiKey ? 'key' : 'user');
  },
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  skip: (req) => req.path === '/healthz',
  handler: (req, res) => {
    res.status(429).json({ error: 'rate limit exceeded', retryAfter: res.getHeader('Retry-After') });
  },
});

const app = express();
app.use('/api/', limiter);

app.get('/api/report', (req, res) => res.json({ ok: true }));
app.get('/healthz', (req, res) => res.json({ status: 'up' }));

app.listen(3000);