PHP限流实战:从入门到精通的4种访问频率控制方案(含防刷策略)
目录导读(Table of Contents)
- 为什么你的接口需要限流?(业务痛点与攻击场景)
- 基于文件锁的简单计数器(零依赖,适合单机)
- Redis滑动窗口算法(高并发首选,防突发流量)
- Redis令牌桶算法(允许平滑突发,应对流量整形)
- Nginx层限流(配合PHP)(前置防御,减轻PHP压力)
- 防绕过实战问答(Q&A):IP伪造、分布式节点、误杀用户体验
- 性能对比与选型建议(决策树)
在构建高可用API时,限制访问频率(Rate Limiting)是保护后端资源、防止接口被恶意刷爆的核心手段,许多PHP开发者会陷入“加Redis就完事”的误区,但方案选型必须结合并发规模、部署架构(单机/集群)以及业务容忍度,本文剔除繁杂理论,直接对比四种经过生产验证的PHP限流实现,并针对高频反爬痛点给出具体代码。

为什么你的接口需要限流?
除了显而易见的DDoS防护,业务层面更需要防“逻辑漏洞”:比如一个活动兑换码接口,如果每分钟允许请求1000次,攻击者只需循环调用即可将库存清空,限流的目标不是拒绝所有用户,而是平滑流量尖峰,保障核心交易链路,PHP作为偏应用层的语言,建议选择轻量级方案,避免引入过重的网关组件。
方案一:基于文件锁的简单计数器(零外部依赖)
适用于单机部署、日均请求量低于10万的项目,利用file_put_contents配合flock实现原子自增:
$key = md5($_SERVER['REMOTE_ADDR'] . date('YmdHi'));
$file = sys_get_temp_dir() . '/rate_' . $key;
$fp = fopen($file, 'c+');
flock($fp, LOCK_EX);
$count = (int)fread($fp, 10);
if ($count >= 60) { http_response_code(429); exit('Too Many'); }
ftruncate($fp, 0); fwrite($fp, $count + 1);
flock($fp, LOCK_UN);
致命缺点:无法处理分布式多节点,且磁盘I/O在高并发下成为瓶颈。
方案二:Redis滑动窗口算法(高并发首选)
在抢购、秒杀场景,滑动窗口能精确控制边界时间(如1秒内允许5次),使用Redis的ZSET有序集合实现:
$key = 'sliding:' . $ip;
$now = microtime(true);
$window = 60; // 60秒窗口
$limit = 5;
// 移除窗口外的时间戳
$redis->zRemRangeByScore($key, 0, $now - $window);
$count = $redis->zCard($key);
if ($count >= $limit) { return false; }
$redis->zAdd($key, $now, $now . ':' . uniqid());
$redis->expire($key, $window); // 防止内存泄漏
优势:内存操作极快,且天然支持分布式。技巧:务必加上expire,否则ZSET会无限增长导致内存溢出。
方案三:Redis令牌桶算法(允许平滑突发)
如果你希望用户能短时间内(比突发流量)用掉历史“余量”,令牌桶是更友好的选择,使用INCR + EXPIRE模拟令牌补充:
$key = 'token:' . $uid;
$capacity = 10; // 桶容量
$tokens = $redis->get($key);
if ($tokens < 1) {
// 每200ms补充一个令牌(简单模拟)
$refillRate = 5; // 每秒补充5个
$lastRefill = $redis->get($key.':time') ?: time();
$addTokens = (time() - $lastRefill) * $refillRate;
$newTokens = min($capacity, $tokens + $addTokens);
$redis->multi()->set($key, $newTokens-1)->set($key.':time', time())->exec();
} else {
$redis->decr($key);
}
注意:真正高精度需使用Lua脚本原子操作,否则会有竞态条件。
方案四:Nginx层限流(前置防御)
对于性能要求极高的接口,应将限流前置到Nginx limit_req 模块,PHP只负责业务逻辑:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=mylimit burst=20 nodelay; # burst允许20个突发连接
fastcgi_pass php-fpm;
}
}
为什么推荐混合使用? Nginx可以挡住99%的恶意流量,PHP内再针对用户ID维度做细粒度限流(如每个用户每秒只能创建一次订单),实现双层防护。
防绕过实战问答(Q&A)
问:攻击者轮换IP怎么办?
答:单靠IP不可靠,应综合User-Agent、Cookie、设备指纹,甚至行为验证码。进阶方案:在Redis中建立IP+会话指纹的复合键,如ssid:gaid:123。
问:多台PHP服务器部署时,方案二和三还能用吗?
答:完全可用,因为Redis是共享存储,但一定要确保Redis使用Lua脚本执行整个限流逻辑,避免并发时计数器加错。
问:限流误伤正常用户,如何优雅提示?
答:不要直接返回错误,应返回403时带上Retry-After头,前端可以自动重试,后端可用try-catch记录被限流的用户ID,后续加入白名单。
问:如何测试限流是否生效?
答:使用ab -n 1000 -c 100压测,观察429状态码比例,更细致的是用tcpdump检查网络包重传率。
性能对比与选型建议(决策树)
- 单机小站,懒人方案:文件锁(方案一),代码5分钟搞定,无需安装扩展。
- 有Redis实例,标准场景:滑动窗口(方案二),适合控制频率,但突发流量可能短时拒绝用户。
- 电商秒杀,允许突发:令牌桶(方案三),配合
Lua脚本,建议限流key的过期时间为窗口的两倍。 - 超大流量API:Nginx+PHP双层(方案四+二),Nginx做最前端粗粒度,PHP针对业务ID细粒度。
最终建议:最佳实践是Nginx(IP/连接数级) + PHP(用户动作级) + Redis(计数器存储) 三合一架构,切莫将所有压力给到PHP进程,因为PHP-FPM的并发能力远低于Nginx的事件驱动模型。
题外话:限流返回的HTTP状态码统一用429 Too Many Requests,并在响应头中携带X-RateLimit-Remaining字段,方便开发者调试,这不仅是技术规范,也是SEO友好(防止搜索引擎爬虫被限流后降权)。
(全文完)