PHP项目中如何实现接口限速?

wen PHP项目 3

本文目录导读:

PHP项目中如何实现接口限速?

  1. 基于Redis的滑动窗口算法
  2. 基于令牌桶算法
  3. 基于文件系统的简单实现
  4. 使用中间件封装(推荐)
  5. Nginx级别的限速
  6. 最佳实践建议

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

最佳实践建议

  1. 选择合适算法

    • 滑动窗口:适合精确限速
    • 令牌桶:允许短时间突发
    • 漏桶算法:平滑请求
  2. 返回合适的HTTP头

    header('X-RateLimit-Limit: ' . $limit);
    header('X-RateLimit-Remaining: ' . $remaining);
    header('X-RateLimit-Reset: ' . $resetTime);
  3. 分布式环境:使用Redis等中心化存储

  4. 粒度设计

    • 按IP限速
    • 按用户ID限速
    • 按API路径限速
    • 组合使用
  5. 异常处理:限速失败时应该放行而不是拒绝所有请求

对于生产环境,建议使用成熟的库如 nikic/FastRoute 配合Redis实现,或者采用API网关来处理限速逻辑。

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