PHP接口限流需匹配场景选算法:固定窗口易漏防且窗口切换时并发超限;滑动窗口须zRemRangeByScore与zCount配对执行并设key过期;令牌桶必须用Lua脚本原子化操作,Redis故障时应放行而非阻断。

PHP接口限流不是“加个Redis键就完事”,关键在选对算法、写对逻辑、扛住异常。固定窗口最简单但会漏防,滑动窗口精度高但Redis操作必须成对出现,令牌桶适合突发流量但状态更新必须原子化——三者没有优劣,只有是否匹配你的场景。
固定窗口限流为什么一上线就出问题
它用 INCR + EXPIRE 实现,看似一行代码就能拦住超量请求,但实际踩坑点密集:
- 窗口切换瞬间(如 13:59:59 和 14:00:00)可能各来 100 次,真实 1 秒内承受 200 次,后端直接抖动
-
INCR返回值是自增后的结果,判断条件写成$current > $limit就会少拦 1 次 - 没做
EXPIRE的兜底检查:若第一次INCR成功但EXPIRE失败,这个 key 会永久存在,后续所有请求都被误拦 - 用户标识若只用
$_SERVER['REMOTE_ADDR'],NAT 环境下多个用户共用一个 IP,容易误伤
示例中正确写法:
$key = 'rate_limit:ip:' . $_SERVER['REMOTE_ADDR'];
$current = $redis->incr($key);
if ($current == 1) {
$redis->expire($key, 60); // 必须在 first hit 时设过期
}
if ($current > 100) {
http_response_code(429);
exit('Too many requests');
}
滑动窗口必须配对使用 zRemRangeByScore 和 zCount
很多人只写 zCount 统计,忘了清理旧数据,ZSET 持续膨胀直到 Redis 内存打满。这不是性能问题,是稳定性事故。
-
zRemRangeByScore($key, 0, $window_start)必须在zCount前执行,否则统计结果包含过期请求 - 时间戳必须用
time(),不能用microtime(true)—— Redis 的ZCOUNT只支持整数 score,小数会被截断,导致统计错乱 - 要给 key 加
expire($key, $window_seconds + 10),防止某次清理失败后 key 永久残留 - 如果同时按 IP 和用户 ID 限流,别复用同一个 key,比如
rate_limit:ip_1.2.3.4和rate_limit:user_123必须分离
令牌桶用 Redis 实现时,千万别手写填充逻辑
PHP 层计算时间差再更新令牌数,会在并发下丢失状态。例如两个请求几乎同时到达,都读到 tokens = 5,各自加了 0.3 个,又都写回 5,实际该变成 5.6。
- 必须用 Redis Lua 脚本保证原子性:读取当前
tokens和last_fill_time、计算新增令牌、更新两者、判断并扣减,全部在一个 EVAL 中完成 - 桶容量和速率要存进脚本参数,不要硬编码在 Lua 里,方便运行时动态调整
- 如果 Redis 不可用,
eval抛出Predis\Connection\ConnectionException,此时应记录告警但返回true(放行),不能让限流成为单点故障源 - 测试时注意时钟漂移:PHP 服务器和 Redis 服务器系统时间差超过 1 秒,令牌生成速率就会严重失准
真正难的从来不是“怎么写”,而是“怎么不崩”——滑动窗口漏清理、令牌桶没原子化、固定窗口没防窗口跳跃,这些问题在线上不会报错,只会悄悄拖慢响应、吃光内存、压垮下游。盯住 Redis 连接健康度、监控 ZSET cardinality、定期抽查 key TTL,比多写一个算法重要得多。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











