本文目录导读:

在PHP项目中实现接口限流,常见的方法有以下几种,我会给出具体的代码实现:
基于文件系统的简单计数器
<?php
class FileRateLimiter {
private $limit;
private $window;
private $storagePath;
public function __construct($limit = 100, $window = 60, $storagePath = '/tmp/rate_limits/') {
$this->limit = $limit;
$this->window = $window;
$this->storagePath = $storagePath;
if (!is_dir($storagePath)) {
mkdir($storagePath, 0777, true);
}
}
public function check($key) {
$file = $this->storagePath . md5($key) . '.txt';
$current = time();
$requests = [];
if (file_exists($file)) {
$content = file_get_contents($file);
$requests = json_decode($content, true) ?? [];
}
// 清理过期请求
$requests = array_filter($requests, function($timestamp) use ($current) {
return $timestamp > ($current - $this->window);
});
// 检查是否超限
if (count($requests) >= $this->limit) {
return false;
}
// 记录新请求
$requests[] = $current;
file_put_contents($file, json_encode($requests));
return true;
}
}
// 使用示例
$limiter = new FileRateLimiter(100, 60);
$key = $_SERVER['REMOTE_ADDR']; // 基于IP限制
if (!$limiter->check($key)) {
http_response_code(429);
echo json_encode(['error' => '请求过于频繁,请稍后再试']);
exit;
}
基于Redis的滑动窗口算法
<?php
class RedisRateLimiter {
private $redis;
private $limit;
private $window;
public function __construct($limit = 100, $window = 60) {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->limit = $limit;
$this->window = $window;
}
public function check($key) {
$current = microtime(true);
$redisKey = "rate_limit:{$key}";
// 移除窗口外的旧记录
$this->redis->zRemRangeByScore($redisKey, 0, $current - $this->window);
// 获取当前窗口内的请求数
$count = $this->redis->zCard($redisKey);
if ($count >= $this->limit) {
return false;
}
// 添加新请求
$this->redis->zAdd($redisKey, $current, $current);
$this->redis->expire($redisKey, $this->window);
return true;
}
// 获取剩余请求数
public function getRemaining($key) {
$redisKey = "rate_limit:{$key}";
$current = microtime(true);
$this->redis->zRemRangeByScore($redisKey, 0, $current - $this->window);
$count = $this->redis->zCard($redisKey);
return max(0, $this->limit - $count);
}
}
// 使用示例
$limiter = new RedisRateLimiter(100, 60);
$key = $_SERVER['REMOTE_ADDR'] . ':' . $_SERVER['REQUEST_URI'];
if (!$limiter->check($key)) {
header('HTTP/1.1 429 Too Many Requests');
header('Retry-After: ' . 60);
echo json_encode([
'error' => '请求过于频繁',
'retry_after' => 60
]);
exit;
}
令牌桶算法实现
<?php
class TokenBucket {
private $redis;
private $capacity; // 桶容量
private $rate; // 令牌速率(个/秒)
private $tokensKey; // 令牌数量key
private $lastTimeKey; // 最后更新时间key
public function __construct($capacity = 100, $rate = 10) {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->capacity = $capacity;
$this->rate = $rate;
}
public function consume($key, $tokens = 1) {
$script = <<<LUA
local key_tokens = KEYS[1] .. '_tokens'
local key_time = KEYS[1] .. '_time'
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local tokens_needed = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local tokens = redis.call('GET', key_tokens)
local last_time = redis.call('GET', key_time)
if tokens == false then
tokens = capacity
else
tokens = tonumber(tokens)
end
if last_time == false then
last_time = now
else
last_time = tonumber(last_time)
end
-- 计算新增令牌
local elapsed = now - last_time
local new_tokens = math.floor(elapsed * rate)
if new_tokens > 0 then
tokens = math.min(capacity, tokens + new_tokens)
end
local result = 0
if tokens >= tokens_needed then
tokens = tokens - tokens_needed
redis.call('SET', key_tokens, tokens)
redis.call('SET', key_time, now)
result = 1
end
return result
LUA;
return $this->redis->eval($script, [$key], 1,
$this->capacity, $this->rate, $tokens, microtime(true));
}
}
// 使用示例
$bucket = new TokenBucket(100, 10); // 容量100,每秒生成10个令牌
$key = "api:user:" . getUserId();
if (!$bucket->consume($key, 1)) { // 消耗1个令牌
http_response_code(429);
echo json_encode(['error' => '请求限流']);
exit;
}
中间件方式实现(ThinkPHP示例)
<?php
namespace app\middleware;
use think\facade\Cache;
class RateLimit
{
public function handle($request, \Closure $next)
{
$key = $this->getLimitKey($request);
$limit = 100; // 最大请求数
$window = 60; // 时间窗口(秒)
// 使用缓存实现
$requests = Cache::get($key, []);
$now = time();
// 清理过期记录
$requests = array_filter($requests, function($time) use ($now, $window) {
return $time > ($now - $window);
});
if (count($requests) >= $limit) {
return json([
'code' => 429,
'message' => '请求过于频繁',
'retry_after' => $window - ($now - $requests[0])
], 429);
}
$requests[] = $now;
Cache::set($key, $requests, $window);
return $next($request);
}
private function getLimitKey($request)
{
// 基于IP和路由生成唯一key
$ip = $request->ip();
$route = $request->pathinfo();
$userId = session('user_id') ?? 'guest';
return "rate_limit:{$userId}:{$ip}:{$route}";
}
}
使用Composer包
简化开发,可以使用现有包:
composer require predis/predis composer require mrluki/rate-limiter
<?php
use Mrluki\RateLimiter\RateLimiter;
use Mrluki\RateLimiter\Storage\RedisStorage;
$client = new Predis\Client('tcp://127.0.0.1:6379');
$storage = new RedisStorage($client);
$limiter = new RateLimiter($storage);
// 配置:每分钟100次请求
$limiter->setLimit(100)->setWindow(60);
if (!$limiter->check('api_key:' . $apiKey)) {
header('HTTP/1.1 429 Too Many Requests');
exit('Rate limit exceeded');
}
Nginx层面限流(推荐配合使用)
直接在Nginx配置,性能更好:
# 定义限流区域
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
server {
location /api/ {
# 应用限流
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://php_backend;
}
}
最佳实践建议
- 选择合适的粒度:根据业务需求决定限制粒度(用户/IP/接口/全局)
- 分布式环境:使用Redis而非文件系统
- 返回友好提示:包含重试时间和当前限制信息
- 组合使用:Nginx做基础限流 + PHP做业务限流
- 异常处理:确保限流失败时保持系统可用
- 监控告警:记录限流触发情况,及时调整策略
选择哪种实现取决于你的项目规模、技术栈和性能要求,对于大多数中小型项目,Redis滑动窗口算法已经足够使用。