登录、发验证码、开放 API 都需要限流。用 Redis 实现时,常见的有三种算法。
固定窗口
每个时间窗口一个计数器,超过上限就拒绝。实现最简单,缺点是窗口交界处可能在短时间内放过两倍的请求。
Lua
-- KEYS[1]: 计数键 ARGV[1]: 窗口秒数 ARGV[2]: 上限
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
return 0
end
return 1INCR 和 EXPIRE 要放在同一个 Lua 脚本里原子执行。分两条命令发的话,进程恰好在中间崩溃,就会留下一个永不过期的键,这个用户会被一直拦住。
滑动窗口
用有序集合记录每次请求的时间戳,先 ZREMRANGEBYSCORE 删掉窗口外的记录,再用 ZCARD 计数。结果精确,但每个请求占一条记录,流量大时内存开销明显。
令牌桶
按固定速率往桶里放令牌,每个请求取走一个,桶空了就拒绝。既允许一定的突发,又能限制平均速率,适合对外开放的 API。
怎么选
- 登录、验证码这类防刷场景:固定窗口足够。
- 要求“任意 60 秒内不超过 N 次”:滑动窗口。
- 按用户或应用配额的开放接口:令牌桶。
另外两点:限流的键要带上维度(用户 ID 或 IP 加接口名);被限流时返回 HTTP 429,并带上 Retry-After 头告诉客户端多久后再试。