四种算法对比
| 算法 | 特点 | 边界问题 | 适用 |
|---|---|---|---|
| 固定窗口 | 按整段时间计数 | 窗口交界出现双倍突发 | 简单统计 |
| 滑动窗口 | 近似平滑 | 实现稍复杂 | 更均匀的限流 |
| 令牌桶 | 允许一定突发 | 需维护桶容量 | 面向用户的 API |
| 漏桶 | 恒定速率流出 | 突发会被排队或丢弃 | 保护下游、削峰 |
分布式计数
多实例下要用共享存储:Redis 的 INCR + EXPIRE 或 Lua 脚本保证原子;否则每台机器各算各的,等于没限。
三个关键做法
- 返回 429 + Retry-After:让客户端知道何时重试,而不是盲目重试;
- 按身份维度限流:用户、API Key、租户分别计数,别只按 IP(NAT 下会误伤一群人);
- 配合退避抖动:客户端重试加指数退避和随机抖动,避免重试风暴把服务打挂。
和幂等、重试的关系
限流解决「别打太猛」,幂等解决「重试会不会重复生效」。两者配合,重试才安全。相关:API 幂等、重试与超时。
实战案例:三种限流引发的问题
- 固定窗口在边界被打爆:窗口切换瞬间两倍流量涌入。改用滑动窗口或令牌桶可平滑这一突刺。
- 只按 IP 限流:同一 NAT 出口后的正常用户被连带限流,攻击者换个 IP 即可绕过。应按用户、令牌或接口维度组合限流,IP 仅作兜底。
- 被限流却返回 500:客户端无法区分“稍后重试”与“服务故障”,重试风暴加剧拥堵。应返回 429 并带上
Retry-After。
常见问题(FAQ)
令牌桶与漏桶怎么选?令牌桶允许配置范围内的突发流量,漏桶输出更平滑;面向用户的接口通常用令牌桶。限流状态存哪?多实例必须用集中存储(如 Redis)共享计数,否则每个实例各限一份,实际放行量翻倍。要区分接口吗?要,登录、短信、导出等昂贵接口应单独设更严格的阈值。阈值怎么定?先压测得出单实例容量,再按用户与全局分别设定,并留出安全余量。
与业务策略的配合
限流不是孤立的中间件配置,它需要与业务规则配合才能真正有效。
- 分级而非一刀切:为不同接口设定不同阈值——登录与短信接口最严,查询类接口可宽松。用同一套阈值覆盖所有接口,要么误伤正常业务,要么对昂贵接口保护不足。
- 区分正常突发与异常扫描:允许用户短时间内连续操作的正常模式,同时对均匀高频、遍历式访问提高警惕。仅按次数限流会把正常用户和爬虫一起拦下。
- 给调用方明确反馈:除了返回状态码与重试时间,还应在响应头中给出剩余额度与重置时间,让客户端能自我调整,而不是靠反复试错。
- 限流要能临时调整:大促、活动或故障期间需要动态放宽或收紧。把阈值做成可热更新的配置,比改代码重新发布快得多。
- 防止绕过:同一用户的多个来源地址、多设备登录都应能被聚合到同一维度,否则攻击者只需切换地址即可绕过。
上线后的校验
限流上线后应验证三件事:正常用户的误伤率是否可接受、被限流的请求是否确实异常、以及限流本身是否引入了额外延迟。缺少校验的限流往往在真正发生攻击时才发现阈值形同虚设。
与计费、配额的衔接
限流之外,往往还需要配额管理:前者防止瞬时过载,后者控制总量并支撑计费。两者应基于同一套计量口径,否则会出现“限流没触发但配额已耗尽”这类难以向用户解释的情况。计量数据的采集精度与延迟也应与用户可见的用量保持一致。