PHP项目接口限流如何代码实现

wen PHP项目 22

本文目录导读:

PHP项目接口限流如何代码实现

  1. 基于文件系统的简单计数器
  2. 基于Redis的滑动窗口算法
  3. 令牌桶算法实现
  4. 中间件方式实现(ThinkPHP示例)
  5. 使用Composer包
  6. Nginx层面限流(推荐配合使用)
  7. 最佳实践建议

在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;
    }
}

最佳实践建议

  1. 选择合适的粒度:根据业务需求决定限制粒度(用户/IP/接口/全局)
  2. 分布式环境:使用Redis而非文件系统
  3. 返回友好提示:包含重试时间和当前限制信息
  4. 组合使用:Nginx做基础限流 + PHP做业务限流
  5. 异常处理:确保限流失败时保持系统可用
  6. 监控告警:记录限流触发情况,及时调整策略

选择哪种实现取决于你的项目规模、技术栈和性能要求,对于大多数中小型项目,Redis滑动窗口算法已经足够使用。

抱歉,评论功能暂时关闭!