本文目录导读:

在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滑动窗口算法已经足够使用。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播