
限流算法
固定窗口
简单,但允许在窗口边界突发。
滑动窗口
更平滑的分布,在滚动窗口内计数请求。
令牌桶
允许受控突发,同时强制执行平均速率。
express-rate-limit(基础)
npm install express-rate-limit
import rateLimit from 'express-rate-limit'
// 全局限流
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: '请求过多,请稍后再试。' },
}))
// 认证端点的严格限流
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // 仅计数失败尝试
})
app.use('/auth/login', authLimiter)
基于 Redis 的滑动窗口
import Redis from 'ioredis'
const redis = new Redis()
async function slidingWindowLimit(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
const now = Date.now()
const windowStart = now - windowMs
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, '-inf', windowStart) // 移除旧条目
pipeline.zadd(key, now, `${now}-${Math.random()}`) // 添加当前请求
pipeline.zcard(key) // 窗口内计数
pipeline.pexpire(key, windowMs) // 设置过期
const results = await pipeline.exec()
const count = results?.[2]?.[1] as number ?? 0
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: new Date(now + windowMs),
}
}
// 中间件
app.use(async (req, res, next) => {
const identifier = req.user?.id ?? req.ip
const { allowed, remaining, resetAt } = await slidingWindowLimit(
`rate:${identifier}`,
100,
60 * 1000
)
res.set({
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': resetAt.toISOString(),
})
if (!allowed) {
return res.status(429).json({ error: '超出速率限制' })
}
next()
})
令牌桶算法
class TokenBucket {
private tokens: number
private lastRefill: number
constructor(
private capacity: number,
private refillRate: number, // 每秒令牌数
) {
this.tokens = capacity
this.lastRefill = Date.now()
}
async consume(tokens = 1): Promise<boolean> {
this.refill()
if (this.tokens < tokens) return false
this.tokens -= tokens
return true
}
private refill() {
const now = Date.now()
const elapsed = (now - this.lastRefill) / 1000
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate)
this.lastRefill = now
}
}
// 使用 Redis Lua 脚本的分布式令牌桶
const consumeToken = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = (now - last_refill) / 1000
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_refill', now)
redis.call('pexpire', key, 60000)
return 1
end
return 0
`
async function consumeTokenRedis(userId: string): Promise<boolean> {
const result = await redis.eval(
consumeToken, 1,
`bucket:${userId}`,
100, // 容量
10, // 填充速率(令牌/秒)
Date.now(),
1 // 消耗 1 个令牌
)
return result === 1
}
分层限流
const tiers = {
free: { requests: 100, windowMs: 60 * 60 * 1000 },
pro: { requests: 1000, windowMs: 60 * 60 * 1000 },
enterprise: { requests: 10000, windowMs: 60 * 60 * 1000 },
}
app.use(async (req, res, next) => {
const tier = req.user?.subscription ?? 'free'
const { requests, windowMs } = tiers[tier]
const { allowed } = await slidingWindowLimit(`rate:${req.user?.id}`, requests, windowMs)
if (!allowed) return res.status(429).json({ error: '超出速率限制' })
next()
})