本文目录导读:

在 PHP 中实现“流量管理”(Traffic Management)通常可以从四个不同层面来理解和实施,根据你的具体需求(是限制用户访问频率、控制服务器负载,还是做网络层面的限流),以下是具体的方案和代码示例:
应用层限流(最常见的需求)
这是指限制单个用户(IP、UID)在特定时间窗口内访问 API 或页面的次数。
A. 使用文件/数据库(简单方案)
这种方式适合没有 Redis 扩展的廉价虚拟主机。
<?php
class RateLimiter {
private $storageDir = '/tmp/rate_limit/'; // 确保可写
public function isAllowed($key, $maxRequests = 10, $windowSeconds = 60) {
$file = $this->storageDir . md5($key) . '.txt';
$now = time();
$data = ['count' => 0, 'start_time' => $now];
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
// 窗口过期,重置
if (($now - $data['start_time']) > $windowSeconds) {
$data = ['count' => 0, 'start_time' => $now];
}
}
if ($data['count'] >= $maxRequests) {
return false; // 拒绝访问
}
// 增加计数并写回
$data['count']++;
$data['expire_at'] = $data['start_time'] + $windowSeconds;
file_put_contents($file, json_encode($data), LOCK_EX);
// 清理过期文件(可按需定期清理)
return true;
}
}
// 使用示例
$limiter = new RateLimiter();
$userKey = $_SERVER['REMOTE_ADDR']; // 按IP限流
if (!$limiter->isAllowed($userKey, 5, 60)) {
http_response_code(429);
die('Too Many Requests');
}
B. 使用 Redis(推荐方案,适用于高并发)
Redis 的 INCR 和 EXPIRE 是最优雅的限流方式。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
function rate_limit($redis, $key, $limit, $expire) {
$current = $redis->get($key);
if ($current === false) {
// 第一次访问,设置值并设置过期时间
$redis->multi();
$redis->set($key, 1);
$redis->expire($key, $expire);
$redis->exec();
return true;
} elseif ($current < $limit) {
$redis->incr($key);
return true;
} else {
return false; // 触发限流
}
}
$key = 'user:' . $_SERVER['REMOTE_ADDR'];
if (!rate_limit($redis, $key, 10, 60)) {
header('HTTP/1.1 429 Too Many Requests');
exit(json_encode(['error' => '请求太频繁,请稍后再试']));
}
使用滑动窗口算法(防止“突发流量”)
上面的固定窗口算法在窗口切换瞬间会允许双倍流量,如果需要更精确的控制,可以使用滑动窗口,借助 Redis 的 ZSET(有序集合):
<?php
$key = 'sliding_window:' . $ip;
$now = microtime(true);
$window = 60; // 1分钟
$limit = 10;
$redis->multi();
$redis->zRemRangeByScore($key, 0, $now - $window); // 移除过期记录
$redis->zAdd($key, $now, uniqid()); // 添加当前请求
$count = $redis->zCard($key); // 获取窗口内总数
$redis->expire($key, $window); // 设置过期时间
$result = $redis->exec();
if ($count > $limit) {
// 超限,需要移除本次添加的记录(或者标记拒绝)
die("请求过于频繁");
}
服务器层(Nginx 层限流)
如果代码运行在 Nginx 之后,在 Nginx 层面做限制性能消耗最低,并且可以在业务到达 PHP 之前拦截。
在 Nginx 配置中:
# http 块
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /api/ {
# burst=20 允许突发20个请求进入队列,nodelay 表示不等待直接拒绝
limit_req zone=mylimit burst=20 nodelay;
# 添加响应头方便调试
limit_req_status 429;
# 转发给 PHP-FPM
include fastcgi_params;
}
}
流量整形/熔断(服务降级)
当流量过大时,除了硬性拒绝(限流),还可以采用负载保护:
- 使用
pcntl_fork或Swoole统计服务器当前负载。 - PHP 进程的
sys_getloadavg()返回的 1 分钟负载超过 4.0(具体视CPU核数),直接返回“系统繁忙”页面,避免数据库崩溃。
<?php
$load = sys_getloadavg();
if ($load[0] > 4.0) { // 如果当前负载大于4
http_response_code(503);
die('Server is busy, please try again later.');
}
总结与建议
| 场景 | 推荐方案 |
|---|---|
| 小规模项目/虚拟主机 | 文件锁(第一种) |
| API接口开发/微服务 | Redis + 固定窗口 或 滑动窗口 |
| 高并发入口流量 | Nginx limit_req 模块 |
| 防止数据库压力过大 | 检查 sys_getloadavg() 做熔断 |
最佳实践提示:在生产环境,建议Nginx 做第一层粗粒度拦截(限IP),PHP + Redis 做第二层细粒度拦截(针对用户Token/API Key),同时返回的 429 状态码应包含 Retry-After 头部,方便客户端(如 App)自动等待重试。