示例说明
多实例部署下内存计数器各算各的,总额度被放大成实例数倍。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);