Redis滑动窗口限流最可靠,因其原子操作、过期机制与单线程特性保障精确计数、自动清理和无竞态;需用Lua脚本保证incr+expire原子性,ZSET实现滑动窗口,多级键设计,可信代理头获取真实IP。

用 Redis 实现滑动窗口限流最可靠
PHP 自身没有原生限流能力,靠 sleep() 或文件锁既不准又难扩展。生产环境必须用外部存储,Redis 是事实标准——它支持原子操作、过期键和 INCR/EXPIRE 组合,能精准实现滑动窗口(如“每分钟最多 60 次”)。别用 MySQL 记录请求时间戳,高并发下写入瓶颈和事务开销会直接拖垮接口。
实操建议:
- 用
Redis::pipeline()批量执行INCR和EXPIRE,避免两次网络往返 - 窗口粒度设为秒级(如 1 秒内最多 5 次),再用前端聚合判断“每分钟”,比直接存分钟级 key 更平滑
- key 命名用
"rate_limit:{$ip}:{$api_path}",避免不同用户或接口互相干扰 - 注意
Redis连接池配置,短连接在限流场景下极易打满连接数
$_SERVER['REMOTE_ADDR'] 不足以识别真实客户端
只要经过 Nginx、CDN 或云 WAF,$_SERVER['REMOTE_ADDR'] 就是代理 IP,所有用户会被判成同一个来源,导致误封或失效。必须逐层检查 HTTP_X_FORWARDED_FOR、HTTP_X_REAL_IP 等头,并只取最左一个非私有地址。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
- 本地开发正常,上线后全站被限流——因为 CDN 回源 IP 全是内网段(如
10.0.0.0/8) - 同一办公室用户集体触发限流——所有请求都带相同
X-Forwarded-For值
安全做法:
- 在 Nginx 配置中用
set_real_ip_from明确可信代理段,再启用real_ip_header X-Forwarded-For - PHP 中用
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)过滤私有/保留地址 - 必要时叠加
$_SERVER['HTTP_USER_AGENT']或设备指纹(如 TLS 指纹哈希)做辅助识别
返回 429 Too Many Requests 时必须带标准响应头
光返回状态码不够。前端或网关依赖 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 这三个头做退避策略。缺失它们,调用方无法判断何时重试,容易陷入死循环重发。
参数差异与性能影响:
-
X-RateLimit-Reset应填 Unix 时间戳(秒级),不是毫秒——很多 SDK 会直接new Date(reset * 1000) - 不要在每次请求都查 Redis 获取剩余次数:用 pipeline 一次读多个 key,或把计数缓存在 PHP-FPM 进程内(仅适用于单机且窗口较长的场景)
- 若用 Swoole,可将计数存在
Swoole\Table,避免 Redis 往返,但需自行处理进程间同步和过期
白名单和突发流量要分开处理
运维、内部系统、支付回调等必须绕过限流,但不能硬编码在逻辑里。应从配置中心或数据库动态加载白名单规则,否则每次变更都要重启服务。同样,活动期间临时放宽限制(如双 11 提升到每秒 200 次),也不能改代码。
实操要点:
- 白名单匹配优先级:API 路径 > 用户 Token > IP 段 > User-Agent 前缀(如
internal-bot/) - 用
Redis SETNX控制“突发配额”开关,比如设置"burst_mode:orders"过期 30 分钟,命中即走宽松策略 - 记录被限流的请求到
syslog或 Kafka,字段至少含 IP、路径、User-Agent、触发阈值——没日志就等于没防护
真正难的是权衡:太严伤体验,太松挡不住爬虫。IP + 接口 + 时间窗口三元组是底线,其他维度(如请求体特征、行为序列)得用专门的风控服务,PHP 层只做第一道过滤。



















