本文目录导读:

在PHP项目中实现接口限速(Rate Limiting)有多种方法,这里介绍几种主流的实现方案:
基于Redis的滑动窗口算法
这是最常用的实现方式,Redis的原子性操作非常适合做限速。
<?php
class RateLimiter {
private $redis;
private $maxRequests; // 最大请求数
private $windowTime; // 时间窗口(秒)
public function __construct($redis, $maxRequests = 100, $windowTime = 60) {
$this->redis = $redis;
$this->maxRequests = $maxRequests;
$this->windowTime = $windowTime;
}
public function check($key) {
$current = time();
$windowStart = $current - $this->windowTime;
// 使用Redis事务保证原子性
$this->redis->multi();
$this->redis->zRemRangeByScore($key, 0, $windowStart);
$this->redis->zAdd($key, $current, uniqid('', true));
$this->redis->expire($key, $this->windowTime + 1);
$count = $this->redis->zCard($key);
$this->redis->exec();
return $count <= $this->maxRequests;
}
}
// 使用示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$limiter = new RateLimiter($redis, 100, 60);
$userId = $_SERVER['REMOTE_ADDR']; // 可以根据IP或用户ID限速
if (!$limiter->check("rate_limit:{$userId}")) {
http_response_code(429);
header('Retry-After: 60');
echo json_encode(['error' => '请求过于频繁,请稍后再试']);
exit;
}
基于令牌桶算法
令牌桶算法可以允许突发的短时间高并发。
<?php
class TokenBucket {
private $redis;
private $capacity; // 桶容量
private $rate; // 令牌生成速率(每秒)
public function __construct($redis, $capacity = 100, $rate = 10) {
$this->redis = $redis;
$this->capacity = $capacity;
$this->rate = $rate;
}
public function consume($key, $tokens = 1) {
$now = microtime(true);
$bucketKey = "bucket:{$key}";
$lastRefillKey = "last_refill:{$key}";
// 使用Lua脚本保证原子性
$lua = <<<LUA
local key = KEYS[1]
local lastRefillKey = KEYS[2]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local rate = tonumber(ARGV[3])
local tokens = tonumber(ARGV[4])
local currentTokens = redis.call('GET', key)
if not currentTokens then
currentTokens = capacity
else
currentTokens = tonumber(currentTokens)
end
local lastRefill = redis.call('GET', lastRefillKey)
if not lastRefill then
lastRefill = now
else
lastRefill = tonumber(lastRefill)
end
-- 计算新生成的令牌
local newTokens = (now - lastRefill) * rate
currentTokens = math.min(capacity, currentTokens + newTokens)
-- 更新最后填充时间
redis.call('SET', lastRefillKey, now)
if currentTokens >= tokens then
redis.call('SET', key, currentTokens - tokens)
return 1
else
redis.call('SET', key, currentTokens)
return 0
end
LUA;
$result = $this->redis->eval($lua, [$bucketKey, $lastRefillKey, $now, $this->capacity, $this->rate, $tokens], 2);
return $result == 1;
}
}
基于文件系统的简单实现
适合没有Redis的小项目:
<?php
class FileRateLimiter {
private $limitDir;
private $maxRequests;
private $interval;
public function __construct($maxRequests = 100, $interval = 60) {
$this->limitDir = sys_get_temp_dir() . '/rate_limits/';
$this->maxRequests = $maxRequests;
$this->interval = $interval;
if (!is_dir($this->limitDir)) {
mkdir($this->limitDir, 0755, true);
}
}
public function check($key) {
$file = $this->limitDir . md5($key) . '.tmp';
$now = time();
// 使用文件锁防止并发
$fp = fopen($file, 'c+');
if (!flock($fp, LOCK_EX)) {
return false;
}
$data = [];
if (filesize($file) > 0) {
$data = json_decode(fread($fp, filesize($file)), true) ?? [];
}
// 清理过期记录
$data = array_filter($data, function($timestamp) use ($now, $key) {
return $timestamp > ($now - $this->interval);
});
if (count($data) >= $this->maxRequests) {
flock($fp, LOCK_UN);
fclose($fp);
return false;
}
$data[] = $now;
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($data));
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
}
使用中间件封装(推荐)
结合框架使用中间件模式,如Laravel:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimitMiddleware {
public function handle($request, Closure $next, $limit = 100, $timeWindow = 60) {
$key = sprintf('rate_limit:%s:%s',
$request->ip(),
$request->path()
);
$current = Redis::get($key) ?: 0;
if ($current >= $limit) {
return response()->json([
'error' => 'Too Many Requests',
'message' => '请稍后再试'
], 429);
}
Redis::multi();
Redis::incr($key);
Redis::expire($key, $timeWindow);
Redis::exec();
return $next($request);
}
}
Nginx级别的限速
如果只需要基本的IP限速,Nginx配置更高效:
# nginx.conf
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://php_backend;
}
}
}
最佳实践建议
-
选择合适算法:
- 滑动窗口:适合精确限速
- 令牌桶:允许短时间突发
- 漏桶算法:平滑请求
-
返回合适的HTTP头:
header('X-RateLimit-Limit: ' . $limit); header('X-RateLimit-Remaining: ' . $remaining); header('X-RateLimit-Reset: ' . $resetTime); -
分布式环境:使用Redis等中心化存储
-
粒度设计:
- 按IP限速
- 按用户ID限速
- 按API路径限速
- 组合使用
-
异常处理:限速失败时应该放行而不是拒绝所有请求
对于生产环境,建议使用成熟的库如 nikic/FastRoute 配合Redis实现,或者采用API网关来处理限速逻辑。