本文目录导读:

在 PHP 项目中配置站点访问限流,通常有 代码层面、服务器层面 和 软件/中间件层面 三种思路,下面分别介绍几种常用且有效的方案,你可以根据项目架构和运维权限选择。
核心原理
限流的本质是计数器 + 时间窗口(或令牌桶、漏桶算法),我们需要记录每个用户/IP 在特定时间内的请求次数,超过阈值则拒绝。
代码层面(使用 PHP 实现,最灵活,无需服务器权限)
这种方式适合共享主机、无权限修改 Nginx/Apache 配置的环境。
基于文件/IP 的简单计数器(适用于小规模站点)
思路:记录每个 IP 的访问次数到文件或 Redis,超过阈值则返回 429 状态码。
<?php
// rate_limit.php
class RateLimiter {
private $storage; // 存储驱动,Redis 或文件
private $maxRequests = 100; // 最大请求数
private $timeWindow = 60; // 时间窗口(秒)
public function __construct($storage) {
$this->storage = $storage;
}
public function isAllowed($identifier, $maxRequests = null, $timeWindow = null) {
$maxRequests = $maxRequests ?? $this->maxRequests;
$timeWindow = $timeWindow ?? $this->timeWindow;
$key = "rate_limit:{$identifier}";
$current = $this->storage->get($key);
if ($current === false) {
// 第一次访问,设置初始值
$this->storage->setex($key, $timeWindow, 1);
return true;
}
// 检查当前时间窗口内的请求次数
$count = (int)$current;
if ($count >= $maxRequests) {
return false; // 限流
}
// 增加计数
$this->storage->incr($key);
return true;
}
}
// 使用示例(假设你用了 Redis)
// $redis = new Redis();
// $redis->connect('127.0.0.1', 6379);
// $limiter = new RateLimiter($redis);
// $ip = $_SERVER['REMOTE_ADDR'];
// if (!$limiter->isAllowed($ip, 100, 60)) {
// http_response_code(429);
// die('Too Many Requests');
// }
?>
注意:如果不使用 Redis,配合文件锁(flock)可以写文件实现,但高并发下性能较差。
使用 Redis 的“滑动窗口”算法(工业级推荐)
Redis 的有序集合(ZSET)非常适合做滑动窗口限流。
<?php
/** 滑动窗口限流 */
function isAllowed($userId, $action = 'api', $maxCount = 100, $window = 60) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "rate_limit:{$action}:{$userId}";
$now = microtime(true);
$start = $now - $window;
// 1. 移除窗口之外的数据
$redis->zRemRangeByScore($key, 0, $start);
// 2. 计算当前窗口内的请求数
$currentCount = $redis->zCard($key);
if ($currentCount >= $maxCount) {
return false;
}
// 3. 记录本次请求
$redis->zAdd($key, $now, $now . '_' . uniqid());
// 设置过期时间,避免内存泄漏
$redis->expire($key, $window + 1);
return true;
}
// 使用
if (!isAllowed($_SERVER['REMOTE_ADDR'])) {
http_response_code(429);
exit('请求过于频繁');
}
优点:精确控制时间窗口,防止边界瞬高流量(不同于固定窗口的“突刺”问题)。
服务器层面(Nginx / Apache,高性能,无代码侵入)
如果可以直接操作 Web 服务器,这是最高效的方式,几乎不消耗 PHP 进程资源。
Nginx 使用 ngx_http_limit_req_module(推荐)
步骤:
- 在
nginx.conf的http块定义限流区域。 - 在
server或location块启用限流。
# /etc/nginx/nginx.conf
http {
# 定义限流区域:基于 binary_remote_addr(客户端IP)
# 分配 10MB 内存空间,速率限制为 10 请求/秒(burst 允许突发 20 个请求)
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
# 可选:限制连接数
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 80;
server_name example.com;
location /api/ {
# 启用限流,burst=20 允许瞬间超过速率限制的请求排队等待
# nodelay 表示不延迟,排队立即处理但超出 burst 则返回 503
limit_req zone=one burst=20 nodelay;
# 超出限制时返回的状态码和提示信息
limit_req_status 429;
}
location / {
# 限制并发连接数(比如防止 CC 攻击)
limit_conn addr 100;
limit_conn_status 429;
}
}
}
关键参数说明:
rate=10r/s:每秒允许 10 个请求。burst=20:瞬间突发最高允许 30(10+20)个请求。nodelay:排队的请求不延迟处理(尽量不要加,或谨慎使用,否则可能超限)。
Apache 使用 mod_ratelimit(较旧)
<IfModule mod_ratelimit.c>
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 400 # 400 KB/s 带宽限速(非请求数限流)
</IfModule>
注意:Apache 的原生工具对请求数限流支持较弱,通常需要结合 mod_qos 或其他第三方模块。
框架/中间件层面
如果你使用了现代 PHP 框架(如 Laravel, Symfony),可以直接使用框架提供的限流功能,非常方便。
Laravel 的 RateLimiter
Laravel 内置了强大的限流器,支持 Redis/数据库驱动。
定义限流规则(在 App\Providers\RouteServiceProvider 或 App\Http\Kernel):
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
// 按 IP 限流,每分钟 100 次
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(100)->by($request->ip());
});
在路由中应用:
Route::middleware('throttle:api')->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
Symfony 的 Rate Limiter(需要安装组件)
composer require symfony/rate-limiter
在 config/packages/framework.yaml 配置:
framework:
rate_limiter:
anonymous_api:
policy: 'fixed_window' # 或 'sliding_window', 'token_bucket'
limit: 100
interval: '1 minute'
然后在控制器中使用 Symfony\Component\RateLimiter\RateLimiterFactory。
针对用户/API Key 的精细限流
对于提供 API 的服务,通常需要针对不同 API Key 做不同额度的限流。
方案: 结合 Redis 哈希表,以 api_key 作为标识,存储该 Key 在时间窗口内的使用情况。
public function checkApiKeyLimit($apiKey) {
$redis = new Redis();
// 从数据库读取该 key 的限额配置(1000 req/min)
$limitConfig = $this->getApiKeyConfig($apiKey);
$key = "apikey:{$apiKey}";
$current = $redis->get($key);
if ($current === false) {
$redis->setex($key, 60, 1);
return true;
}
if ((int)$current >= $limitConfig['max_requests']) {
return false;
}
$redis->incr($key);
return true;
}
总结与建议
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Nginx 限流 | 独享服务器/VPS | 极高性能,不耗费 PHP 资源 | 只能基于 IP,不易动态调整限额 |
| Redis + PHP | 需要按用户/API Key 精细限流 | 灵活,支持复杂逻辑和配额管理 | 依赖 Redis,有一定网络开销 |
| Laravel/Symfony | 使用该框架的情况下 | 开箱即用,集成度高 | 依赖框架,更换框架需重写 |
| 文件/IP 计数 | 无Redis,无服务器权限,小流量 | 简单,无外部依赖 | 性能差(并发极高时文件锁会成为瓶颈),不适合高并发 |
推荐组合:
- 基础防护:Nginx
limit_req(防止 DDOS 和恶意爬虫)。 - 业务层限流:Redis 滑动窗口(例如控制用户每分钟最多发布 1 次评论)。
- 最终保障:PHP 代码层兜底(防止缓存穿透导致的限流失效)。
如果你能提供更多信息(比如是否使用框架、是否拥有服务器 root 权限、项目并发量级别),我可以给出更具体的配置示例。