
什么是速率限制?
速率限制控制客户端在时间窗口内可以向 API 发出的请求数量。如果没有它,单个行为异常的客户端可能会淹没你的服务器,从而降低其他所有人的服务质量。
速率限制的重要性:
- 防御 DoS/DDoS 攻击
- 防止 API 滥用和爬取
- 确保客户端之间的公平资源分配
- 降低基础设施成本
- 捕获意外错误(如紧循环频繁调用 API)

速率限制算法
1. 固定窗口计数器
在固定时间窗口内计数请求(例如,每个从 :00 开始的 60 秒分钟)。
async function isRateLimited(clientId, limit = 100, windowSeconds = 60) {
const key = `rate:${clientId}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSeconds);
return count > limit;
}
问题: 边界突发——客户端可以在 1:59 发出 100 个请求,在 2:00 再发出 100 个,2 秒内发送 200 个请求。

2. 滑动窗口计数器(推荐)
使用当前窗口和前一个窗口的加权计数来近似真正的滑动窗口:
async function isRateLimitedSliding(clientId, limit = 100, windowSeconds = 60) {
const now = Math.floor(Date.now() / 1000);
const currWindow = Math.floor(now / windowSeconds);
const prevWindow = currWindow - 1;
const elapsed = now % windowSeconds;
const [prevCount, currCount] = await redis.mget(
`rate:${clientId}:${prevWindow}`,
`rate:${clientId}:${currWindow}`
);
const weighted = (Number(prevCount) || 0) * (1 - elapsed / windowSeconds)
+ (Number(currCount) || 0);
if (weighted >= limit) return true;
const pipeline = redis.pipeline();
pipeline.incr(`rate:${clientId}:${currWindow}`);
pipeline.expire(`rate:${clientId}:${currWindow}`, windowSeconds * 2);
await pipeline.exec();
return false;
}
3. 令牌桶
桶中持有 N 个令牌。请求消耗令牌。桶以固定速率补充。允许短时突发,同时保持平均速率。
// Token bucket in Redis using Lua (atomic)
const script = `
local key, capacity, rate, now = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
tokens = math.min(capacity, tokens + (now - ts) * rate)
if tokens < 1 then return 0 end
redis.call('HMSET', key, 'tokens', tokens - 1, 'ts', now)
redis.call('EXPIRE', key, 3600)
return 1
`;
async function isAllowed(clientId) {
return (await redis.eval(script, 1,
`bucket:${clientId}`, 100, 1.67, Date.now()/1000)) === 1;
}

速率限制 HTTP 头部
RateLimit-Limit: 100 # 每个窗口的最大请求数
RateLimit-Remaining: 42 # 当前窗口剩余请求数
RateLimit-Reset: 1716854460 # 窗口重置的 Unix 时间戳
Retry-After: 30 # 等待秒数(在 429 响应中)
Express 中间件
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// 全局速率限制
app.use(rateLimit({
windowMs: 60 * 1000, max: 100,
standardHeaders: true, legacyHeaders: false,
store: new RedisStore({ client: redis }),
}));
// 对敏感端点更严格
app.use('/auth/login', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
skipSuccessfulRequests: true,
}));
客户端:处理 429 并重试
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i <= maxRetries; i++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const delay = (parseInt(res.headers.get('Retry-After')) || Math.pow(2, i)) * 1000;
await new Promise(r => setTimeout(r, delay));
}
throw new Error('Max retries exceeded');
}
速率限制策略
- 按 IP: 简单但容易被代理绕过。适用于匿名访问。
- 按 API 密钥: 更可靠,需要身份验证。
- 按端点: 昂贵操作设置更严格的限制(搜索:10/分钟,读取:1000/分钟)。
- 分层: 根据订阅级别(免费/专业/企业)设置不同限制。
→ 使用令牌生成器生成安全的 API 密钥令牌。