redis滑动窗口限流最可靠,因其用zset精确存时间戳、lua脚本保证原子性、自动过期清理,避免文件或db的i/o瓶颈与竞态问题,且支持多维键设计适配真实业务场景。

Redis 滑动窗口限流是当前 PHP 生产环境最可靠的选择,其他方案(如文件计数、DB 计数、固定窗口)在高并发下基本不可用。
为什么不能用 file_put_contents 或数据库做限流
硬写文件或更新数据库会触发磁盘 I/O 和锁竞争,100 QPS 以上就容易丢计数;file_put_contents('count.txt', $n, FILE_APPEND) 在并发下可能覆盖写入,UPDATE rate_table SET count = count + 1 WHERE ip = ? 会锁行甚至锁表。PHP 原生没有原子递增+过期能力,而限流的核心要求是「精确、自动清理、无竞态」——只有 Redis 天然满足。
INCR + EXPIRE 分两步调用会出问题
先 $redis->incr($key) 再 $redis->expire($key, 60) 存在极小但真实的时间窗口:若 incr 成功后进程崩溃或网络中断,key 就永久存在,后续所有请求都会被误拒。
- 必须用 Lua 脚本保证原子性:
$redis->eval("return redis.call('INCR', KEYS[1]) * redis.call('EXPIRE', KEYS[1], ARGV[1])", 1, $key, 60) - 或者改用
INCRBY配合SET key value EX 60 NX组合(但需自己处理 key 不存在时的初始化逻辑) -
phpredis 扩展版本低于 5.3.0 时,
expire()对不存在 key 返回false,容易漏设过期时间
滑动窗口必须用 ZSET,不是 INCR
固定窗口(如每分钟重置)会导致整点流量突增,而滑动窗口要求「任意 60 秒内最多 N 次」,得靠 ZSET 存时间戳实现:
- 每次请求执行:
$redis->zAdd($key, time(), uniqid()) - 清理旧数据:
$redis->zRemRangeByScore($key, 0, time() - 60) - 统计当前窗口请求数:
$redis->zCard($key) - 注意:别用
time()作为 score 主键——同一秒内多请求会覆盖;用microtime(true)更稳妥
限流 key 必须多级设计,不能只用 IP
单靠 rate_limit:{$_SERVER['REMOTE_ADDR']} 会误杀合法用户(比如公司出口 NAT、校园网),也防不住攻击者换代理。真实场景要叠加维度:
- 登录态接口优先用
rate_limit:{$openid}:{$path}(小程序/APP) - 未登录接口可用
rate_limit:{$_SERVER['HTTP_X_REAL_IP']}:{$path}:{$ua_hash}(UA 哈希防脚本批量) - 高危操作如短信发送,再加
device_id或captcha_token作为子维度 - 务必从
$_SERVER['HTTP_X_REAL_IP']取真实 IP,并 fallback 到$_SERVER['HTTP_X_FORWARDED_FOR']的第一个非私有地址(过滤127.0.0.1、10.0.0.0/8等)
真正难的不是写对一行 INCR,而是键设计是否覆盖业务粒度、时间窗口是否对齐业务节奏、以及 Redis 调用是否真的原子——这些细节一旦漏掉,限流就变成摆设。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











