滑动窗口限流比固定窗口更准,因其按时间戳连续切片、用ZSET统计N秒内请求数,避免窗口切换时的“双倍请求”问题。

滑动窗口限流为什么比固定窗口更准
固定窗口(比如每分钟最多100次)会在窗口切换瞬间出现“双倍请求”问题——前一秒末尾和后一秒开头各来100次,实际1秒内就扛了200次。滑动窗口按时间戳连续切片,用 Redis 的 ZSET 存请求时间戳,每次查过去 N 秒内的成员数,结果更贴近真实速率。
实操建议:
- 用
ZADD rate_limit:{user_id} {timestamp} {request_id}记录每次请求 - 用
ZCOUNT rate_limit:{user_id} {start_ts} {end_ts}统计窗口内请求数 - 必须搭配
ZREMRANGEBYSCORE清理过期时间戳,否则内存无限涨 - 注意 Redis 服务端时钟要和应用一致,跨机房部署时慎用系统时间戳
PHP 用 Redis 实现滑动窗口的最小可行代码
别碰自己写时间窗口逻辑,直接交由 Redis 原生命令处理。PHP 层只做组装和判断,避免在 PHP 中遍历或 sleep。
示例(基于 Predis 客户端):
立即学习“PHP免费学习笔记(深入)”;
$redis = new Predis\Client();
$key = 'rate_limit:uid_123';
$window_seconds = 60;
$max_requests = 100;
<p>$now = time();
$window_start = $now - $window_seconds;</p><p>// 清理旧数据(关键!否则 ZSET 持续膨胀)
$redis->zRemRangeByScore($key, 0, $window_start);</p><p>// 统计当前窗口请求数
$count = $redis->zCount($key, $window_start, $now);</p><p>if ($count >= $max_requests) {
throw new Exception('Too many requests');
}</p><p>// 记录本次请求
$redis->zAdd($key, $now, uniqid());
// 设置过期,防止 key 永久残留(哪怕没清理干净也有兜底)
$redis->expire($key, $window_seconds + 10);</p>注意:zCount 和 zRemRangeByScore 都是 O(log N) 复杂度,N 是窗口内请求数,日常接口压测下完全够用;但若单用户每秒几百次,就得考虑分桶或降级为固定窗口。
Redis 连接失败或超时怎么不拖垮接口
限流本身不能成为故障源。一旦 Redis 不可用,直接放行比硬拦更合理——保主流程,丢限流。
实操建议:
- 给 Redis 操作加超时,
Predis可设'timeout' => 0.1(100ms) - 捕获
Predis\Connection\ConnectionException和RedisException,记录告警但返回true(允许通过) - 不要用
try/catch包裹整个限流逻辑再重试,重试只会放大延迟 - 线上可加一个开关配置(如 Redis 不可用时自动切到内存计数),但内存计数仅限单机且需小心进程重启清零
用户标识用什么才不会被绕过
单靠 IP 很容易被代理、CDN 或 NAT 环境打乱;只用 token 不校验绑定关系也会被复用。真正能控住的是「业务身份 + 设备/会话特征」组合。
常见方案对比:
-
uid:最准,但未登录接口无法使用 -
access_token:需确保 token 解析快且不依赖 DB 查询(例如 JWT 本地验签) -
ip + user_agent + referer:适合游客接口,但隐私合规要求高时需用户授权 - 绝对别用
$_SERVER['HTTP_X_FORWARDED_FOR']直接取 IP——前端可伪造
如果接口既支持登录也支持游客,建议分两套 key:rate_limit:uid_{uid} 和 rate_limit:guest_{hash(ip . ua)},并设置不同阈值。
滑动窗口看着简单,真正卡住的从来不是算法,而是 Redis 延迟毛刺、时钟漂移、连接池耗尽、还有那个永远没人改的过期时间硬编码。











