限流不是为了拒绝用户,而是为了让系统在超载时仍然能服务一部分请求。三种常见算法适用场景差别很大,选错了要么挡不住峰值,要么误伤正常流量。
一、固定窗口计数器
最简单的做法:每分钟一个 key,INCR 后判断值是否超过阈值,key 设 60 秒过期。
INCR rate:user:1024
EXPIRE rate:user:1024 60
优点是实现只要几行,缺点是窗口边界会放过双倍流量:用户在 59 秒发 100 次、00 秒再发 100 次,实际一秒内过了 200 次。滑动窗口可以缓解,但要存更多数据。
适合:登录失败次数、短信验证码这类对精度要求不高的粗粒度限制。
二、漏桶:恒定速率放行
请求先入桶,桶以固定速率漏水(放行),桶满则拒绝。无论上游来得多猛,下游收到的都是平滑流量。
优点是保护下游非常稳,缺点是不允许任何突发。哪怕系统此刻很闲,攒着的请求也得排队慢慢出,用户体感延迟会变高。
适合:调用第三方有明确 QPS 限制的接口、消息推送这类必须匀速的场景。
三、令牌桶:允许一定突发
以固定速率往桶里放令牌,请求来时取一个令牌,取不到才拒绝。桶里可以攒令牌,所以空闲一段时间后能承接一波突发。
rate = 100 令牌/秒 # 长期平均速率
burst = 200 # 桶容量,决定能承受多大突发
这是网关层最常用的算法,兼顾了保护和体验。Guava 的 RateLimiter、Nginx 的 limit_req(配合 burst)本质都是这个思路。
四、分布式环境别忘了一致性
单机内存计数在多实例部署下会失效:3 台机器各限 100 QPS,实际是 300。要用 Redis 集中计数,并且用 Lua 脚本保证「判断 + 自增」的原子性,否则并发下会超发。
local c = redis.call('INCR', KEYS[1])
if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return c
五、被限流之后给什么响应
返回 429,带上 Retry-After 头告诉客户端多久后重试;响应体里给一句人能看懂的提示。内部服务之间调用,则配合退避重试(指数退避 + 随机抖动),避免所有实例同时重试造成二次冲击。
小结
粗粒度用固定窗口,需要匀速用漏桶,网关和面向用户的接口用令牌桶。先明确要保护的是下游还是自己,再选算法。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/708
共有 0 条评论