Node.js 接口限流通常怎么设计?
简化版
Node.js 接口限流通常按 IP、用户、接口或租户维度限制请求频率,常见算法有固定窗口、滑动窗口、漏桶和令牌桶。单机可用内存计数,分布式场景通常用 Redis 原子计数或 Lua 脚本,并配合网关限流。
详细版
限流不是只防攻击,也用于保护下游和稳定体验。 简单固定窗口示例:
const key = `rate:${ip}:${Math.floor(Date.now() / 60_000)}`
const count = await redis.incr(key)
await redis.expire(key, 60)
if (count > 100) {
throw new Error('Too Many Requests')
}
真实系统要考虑维度、算法、分布式一致性、误伤、返回码、重试提示和监控。常见响应是 HTTP 429,并带上 Retry-After。
完整版教学
一、限流的目标是保护系统边界
没有限流时,一个异常用户、爬虫或前端 bug 就可能打满服务。 Node.js 本身事件循环轻,但下游数据库、Redis、第三方接口都有容量上限。 限流是在入口处把风险挡住。
请求流量
-> 限流
-> 鉴权
-> 业务
-> DB / Redis / 第三方接口
假设登录接口每秒正常 50 次,突然被刷到每秒 5000 次。 如果不限制,密码校验、数据库查询和短信服务都会被拖垮。
二、限流维度决定公平性
只按 IP 限流很容易误伤公司 NAT 或校园网用户。 只按用户限流又挡不住未登录攻击。 工程里常组合多个维度:IP、用户 ID、接口、租户、设备和 API Key。
| 维度 | 适合场景 | 风险 |
|---|---|---|
| IP | 未登录接口 | NAT 误伤 |
| 用户 ID | 登录后接口 | 账号被盗时仍要辅助 IP |
| API Key | 开放平台 | Key 泄漏风险 |
| 接口路径 | 保护重接口 | 规则维护成本 |
例如发送验证码可以按手机号、IP、设备三重限制。 这样比单一维度更能防刷,也更少误伤正常用户。
三、固定窗口简单但边界突刺明显
固定窗口按时间段计数,例如每分钟最多 100 次。
实现简单,Redis INCR + EXPIRE 就能做。
问题是窗口边界可能放过突刺。
12:00:59 允许 100 次
12:01:00 又允许 100 次
2 秒内实际通过 200 次
如果接口对瞬时流量敏感,固定窗口不够平滑。 但对于管理后台、低风险接口,它仍然是性价比很高的方案。
四、滑动窗口和令牌桶更平滑
滑动窗口按最近一段时间统计,更准确但存储和计算成本更高。 令牌桶按固定速率生成令牌,请求拿到令牌才通过,允许一定突发。 漏桶则更强调固定速率流出。
令牌桶:
桶容量 100
每秒生成 10 个令牌
请求消耗 1 个令牌
桶空则限流
| 算法 | 平滑度 | 实现成本 | 适合场景 |
|---|---|---|---|
| 固定窗口 | 低 | 低 | 简单接口 |
| 滑动窗口 | 高 | 中高 | 精确限制 |
| 令牌桶 | 中高 | 中 | 允许突发 |
| 漏桶 | 高 | 中 | 平滑输出 |
面试回答不需要把所有算法数学化。 关键是能讲出取舍和业务适配。
五、分布式限流要保证原子性
多实例 Node 服务不能用单机内存计数做全局限流。 同一个用户请求可能打到不同实例。 通常用 Redis 做共享计数,并用 Lua 脚本保证检查和更新原子。
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
如果 INCR 成功但 EXPIRE 失败,key 可能永久存在。
Lua 能把多步操作打包成原子逻辑,减少边界 bug。
六、限流响应要对客户端友好
被限流时通常返回 429 Too Many Requests。
响应体要给出明确错误码,响应头可加 Retry-After。
同时日志和监控要记录命中的规则、维度和当前计数。
记忆钩子:限流不是拒绝用户,是保护系统别被浪拍翻。
限流也要能灰度和动态调整。 规则太紧会误伤,太松又保护不了下游。
七、常见误区与追问
- 误区:限流只按 IP 做就够了。 NAT、代理和未登录攻击会让单一 IP 维度不可靠,应组合维度。
- 误区:单机内存限流适合所有部署。 多实例环境下内存计数不共享,全局限流通常要 Redis 或网关。
- 误区:固定窗口没有缺点。 窗口边界会产生突刺,可能短时间通过两倍流量。
- 追问:为什么用 Lua? 保证检查、计数、过期等多步 Redis 操作的原子性。
- 追问:限流返回什么状态码? 常用 429,并可携带
Retry-After提示重试时间。 - 追问:网关限流和应用限流怎么分工? 网关挡通用流量,应用按业务维度做精细规则。
八、加强记忆
限流题按“维度、算法、分布式、响应”记。维度决定公平性,算法决定平滑度,Redis/Lua 保证多实例一致,429 和监控保证可运营。别只写一个计数器,那只是入门版。