接口总被刷?Node + Redis 限流中间件挡住突发量
一、背景:接口不限位流,迟早被刷挂
任何一个对外 API,都可能遇到突发:有人写个脚本循环调、爬虫无脑抓、或者单纯流量高峰。不限流的话,后端资源(连接、CPU、下游依赖)会被瞬间打满,正常用户也跟着 503。限流(Rate Limiting)就是给每个调用方设一道"每秒/每分最多 N 次"的闸,超了就拒,保护服务也保护下游。
本文用 Node(以 Express 为例)+ Redis 写一个限流中间件:按调用方 IP(或 key)计数,窗口内超阈值就返回 429。讲清"为什么用 Redis 而不是内存计数"、"滑动窗口/固定窗口"的区别。具体客户端 API 以官方文档为准。它和前面 DCS 缓存思路同源——都是把高频读写的状态放 Redis。
二、为什么限流状态要放 Redis
如果服务是单进程,内存里用个 Map 计数也行。但生产服务往往多实例(负载均衡后面好几台),每台各自计数,调用方在 A 实例用了 3 次、B 实例用了 3 次,单台看都没超、合计超了——限流失效。把计数放 Redis(集中存储),所有实例共享同一份计数,限流才准。
Redis 还快、支持原子操作(INCR + 过期),正好匹配"每来一次请求 +1、到窗口就清零"的语义。所以中间件的核心就是:请求来了 → INCR 这个 IP 的计数键 → 超了拒、没超放 → 键设个过期时间自动清零。
三、固定窗口限流:中间件实现
const redis = require('redis')
const client = redis.createClient()
function rateLimit({ window = 60, max = 100 } = {}) {
return async (req, res, next) => {
const key = `rl:${req.ip}` // 按 IP 限流
const count = await client.incr(key) // 原子 +1
if (count === 1) await client.expire(key, window) // 首次设窗口过期
if (count > max) {
return res.status(429).json({ error: 'too many requests' })
}
next()
}
}
app.use(rateLimit({ window: 60, max: 100 })) // 每IP每60秒最多100次
client.incr(key) 是原子自增,并发请求也不会数错;count === 1 时给键设 expire(window),让窗口结束后计数自动归零(不手动删、不累积)。超过 max 返回 429(Too Many Requests)并拒绝,否则 next() 放行。把中间件 app.use 挂上,全站接口都受这个闸控制。req.ip 是按来源 IP 区分,也可换成用户 token、API key,按需。
四、固定窗口的漏洞:临界突刺
上面是"固定窗口":每 60 秒一个窗口,窗口开始计数清零。它的漏洞是——假设限制"每分钟 100 次",攻击者在 00:59 发 100 次、01:00 又发 100 次,两波都在各自窗口内合规,但 00:59~01:00 这两秒实际打了 200 次。这叫"临界突刺",固定窗口挡不住。
更平滑的是滑动窗口:限制"任意连续 60 秒内最多 100 次"。实现上常用 Redis 的 Sorted Set(zadd 时间戳、zremrangebyscore 删窗口外、zcard 看数量),或用"滑动日志/令牌桶"算法。滑动窗口没突刺漏洞但实现复杂些、Redis 操作也多些。选哪种看你对"突发容忍度"的要求——普通 API 固定窗口够用,严格场景上滑动窗口或令牌桶。
五、几个工程注意点
第一,限流键怎么选。按 req.ip 最简单,但背后是 NAT/网关时,多个用户共享一个出口 IP,会被一起限(误伤);或者有用户真用很多 IP 绕开。更稳的是按"已认证用户的 ID / API key"限流,未登录才退而用 IP。真实服务建议"用户级优先、IP 兜底"。
第二,429 要带 Retry-After。被限时返回 429,最好加 Retry-After 头告诉客户端多久后能重试,体验好、也减少客户端盲目重试继续被打。配合标准的限流响应头(X-RateLimit-Limit/Remaining)更专业,但最小可用先返回 429 也行。
第三,放行路径别漏。app.use(rateLimit(...)) 默认对全站生效,但健康检查(/health)、登录接口若也被限,可能把自己挡外面。明确哪些路径排除(白名单),别一刀切。
第四,Redis 挂了怎么办。限流依赖 Redis,Redis 挂了中间件就报错、全站不可用——这叫"限流组件成了单点"。生产要有降级:Redis 不可用时,要么放行(牺牲限流保可用)、要么用本地内存兜底限流,按你"可用性 vs 严格限流"的取舍定。别让保护组件把服务拖死。
第五,窗口与阈值要压测。拍脑袋定的 max=100/60s 可能太松(被刷)或太紧(误伤正常用户)。上线前按真实流量压测、看正常峰值是多大,阈值设在正常峰值的 1.5~2 倍比较稳,再观察调。
六、小结
Node + Redis 限流中间件:按调用方(IP/key)INCR 计数、首次设窗口过期、超阈值返 429。用 Redis 是因为多实例共享计数、且原子操作准。固定窗口简单但有"临界突刺"漏洞,严场景换滑动窗口/令牌桶。注意限流键选用户级优先 IP 兜底、429 带 Retry-After、健康检查等路径要排除、Redis 挂了要有降级(别让保护组件拖垮服务)、阈值靠压测定。Redis 客户端与原子命令以官方文档为准。

- 点赞
- 收藏
- 关注作者
评论(0)