周三凌晨两点,某电商平台的促销系统突然崩溃。不是代码bug,也不是数据库挂了——是竞争对手用脚本疯狂调用价格接口,每秒10万次请求直接把服务器打穿。运维团队花了四小时才恢复,损失订单超过200万。
这种事每天都在发生。如果你的API对外开放,或者系统承载高并发流量,限流(Rate Limiting)不是可选项,是保命项。没有它,DDoS攻击、暴力破解、数据爬虫随时能让你的服务"下线"。
但限流不是简单的"数请求个数"。下面这五种算法,从入门到高阶,决定了你的系统能扛住多大压力。
固定窗口计数器:最简单的陷阱
原理很直白:设定一个时间窗口,比如每分钟最多100次请求。从00:00到00:59算一个窗口,满了就拒绝,下个窗口清零重计。
致命弱点在窗口交界处。攻击者在00:58狂刷100次,00:01又刷100次——两秒内200次请求,系统直接过载。这个漏洞太经典,以至于生产环境几乎没人敢单用它。
滑动窗口日志:精准但烧钱
改进方案是记录每个请求的时间戳。系统随时检查"过去60秒内有多少条记录",精确到毫秒级控制。
代价是内存爆炸。百万级用户、每秒上千请求,日志数据能把Redis撑爆。除非你的用户量极小,否则这条路走不通。
滑动窗口计数器:平衡之道
前两者的 hybrid 方案。不存完整日志,而是把窗口切分成更小的子窗口(比如1分钟切成6个10秒块),按权重估算当前窗口的请求量。
误差可控,内存友好,大多数中型系统的选择。
令牌桶:大厂的默认配置
Amazon、Stripe 都在用的算法。想象一个桶,系统以固定速率往里面放令牌,每个请求必须拿到令牌才能通过。桶有容量上限,满了就不再放令牌。
关键特性:允许突发流量。桶里有100个令牌时,瞬间来100个请求都能通过,之后回归匀速。这对用户体验至关重要——没人想每次点击都等匀速放行。
漏桶:平滑流量的手术刀
请求先进入桶,再以固定速率"漏"出去处理。和令牌桶相反,它压制突发,强制流量平滑。
适合内部服务调用,或者对延迟不敏感的后台任务。用户端接口用它,体感会很卡。
动手:Python+Redis 的最小实现
下面这段代码演示固定窗口的核心逻辑,用Redis的pipeline保证操作原子性:
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def is_request_allowed(user_id, limit, window_seconds):
key = f"rate_limit:{user_id}"
pipe = r.pipeline()
pipe.incr(key, 1)
pipe.expire(key, window_seconds)
results = pipe.execute()
return results[0] <= limit
代码看起来能跑,但生产环境有个隐形炸弹:race condition。当两个请求同时检查计数、同时发现"还没超",结果都放行,实际请求数就翻倍了。
真正的解决方案是用Lua脚本在Redis端原子执行"检查+递增"整个流程。上面的代码只是教学演示,上线必改。
热门跟贴