本文目录导读:

在PHP中实现按用户的API限频,推荐以下方案:
基于Token/UserID的Redis方案(推荐)
<?php
class RateLimiter {
private $redis;
private $maxRequests = 100; // 限制次数
private $timeWindow = 60; // 时间窗口(秒)
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
// 滑动窗口算法
public function isAllowed($userId) {
$key = "rate_limit:user:{$userId}";
$now = time();
// 移除窗口外的记录
$this->redis->zRemRangeByScore($key, 0, $now - $this->timeWindow);
// 获取当前窗口内的请求数
$currentCount = $this->redis->zCard($key);
if ($currentCount >= $this->maxRequests) {
return false;
}
// 添加当前请求
$this->redis->zAdd($key, $now, $now . ':' . uniqid());
$this->redis->expire($key, $this->timeWindow);
return true;
}
}
// 使用示例
$limiter = new RateLimiter();
$userId = $_SERVER['HTTP_X_USER_ID'] ?? $_GET['user_id'];
if (!$limiter->isAllowed($userId)) {
http_response_code(429);
echo json_encode(['error' => 'Too Many Requests']);
exit;
}
// 正常处理请求
基于数据库的方案(简单实现)
<?php
// 数据库表结构
/*
CREATE TABLE api_rate_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
window_start TIMESTAMP NOT NULL,
request_count INT DEFAULT 0,
UNIQUE KEY idx_user_window (user_id, window_start)
);
*/
class DBRateLimiter {
private $db;
private $maxRequests = 100;
private $timeWindow = 60;
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
}
public function isAllowed($userId) {
$windowStart = date('Y-m-d H:i:s', time() - $this->timeWindow);
try {
$this->db->beginTransaction();
// 清理过期记录
$stmt = $this->db->prepare("DELETE FROM api_rate_limits WHERE window_start < ?");
$stmt->execute([$windowStart]);
// 尝试插入或更新
$stmt = $this->db->prepare("
INSERT INTO api_rate_limits (user_id, window_start, request_count)
VALUES (?, NOW(), 1)
ON DUPLICATE KEY UPDATE request_count = request_count + 1
");
$stmt->execute([$userId]);
// 检查是否超限
$stmt = $this->db->prepare("
SELECT COUNT(*) as total
FROM api_rate_limits
WHERE user_id = ? AND window_start >= ?
");
$stmt->execute([$userId, $windowStart]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->db->commit();
return $result['total'] <= $this->maxRequests;
} catch (Exception $e) {
$this->db->rollBack();
return false;
}
}
}
中间件实现的完整方案
<?php
// Middleware接口
interface RateLimitMiddleware {
public function handle($request, $next);
}
// Token Bucket算法实现
class TokenBucketRateLimiter implements RateLimitMiddleware {
private $redis;
private $config = [
'capacity' => 100, // 桶容量
'refillRate' => 10, // 每秒补充速率
'refillTime' => 1 // 补充间隔(秒)
];
public function __construct($config = []) {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->config = array_merge($this->config, $config);
}
public function handle($request, $next) {
$userId = $this->getUserId($request);
$key = "token_bucket:{$userId}";
// 获取或初始化令牌桶
$bucket = $this->redis->hGetAll($key);
if (empty($bucket)) {
$bucket = [
'tokens' => $this->config['capacity'],
'last_refill' => time()
];
$this->redis->hMSet($key, $bucket);
$this->redis->expire($key, 3600);
}
// 补充令牌
$now = time();
$elapsed = $now - $bucket['last_refill'];
$tokensToAdd = floor($elapsed * $this->config['refillRate']);
if ($tokensToAdd > 0) {
$bucket['tokens'] = min(
$this->config['capacity'],
$bucket['tokens'] + $tokensToAdd
);
$bucket['last_refill'] = $now;
$this->redis->hMSet($key, $bucket);
}
// 检查是否有可用令牌
if ($bucket['tokens'] < 1) {
http_response_code(429);
header('Retry-After: ' . (1 / $this->config['refillRate']));
echo json_encode([
'error' => 'Rate limit exceeded',
'retry_after' => 1 / $this->config['refillRate']
]);
exit;
}
// 消耗令牌
$this->redis->hIncrBy($key, 'tokens', -1);
// 设置响应头
header('X-RateLimit-Limit: ' . $this->config['capacity']);
header('X-RateLimit-Remaining: ' . ($bucket['tokens'] - 1));
return $next($request);
}
private function getUserId($request) {
// 根据API密钥或Token获取用户ID
return $request['user_id'] ?? $_SERVER['HTTP_AUTHORIZATION'];
}
}
按不同用户级别配置限制
<?php
class TieredRateLimiter {
private $redis;
private $tiers = [
'free' => ['limit' => 100, 'window' => 3600], // 每小时100次
'basic' => ['limit' => 1000, 'window' => 3600], // 每小时1000次
'premium' => ['limit' => 10000, 'window' => 3600], // 每小时10000次
'enterprise' => ['limit' => 100000, 'window' => 3600]
];
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function checkRateLimit($userId, $userTier = 'free') {
$tier = $this->tiers[$userTier] ?? $this->tiers['free'];
$key = "rate_limit:{$userTier}:{$userId}";
$currentCount = $this->redis->incr($key);
if ($currentCount === 1) {
$this->redis->expire($key, $tier['window']);
}
if ($currentCount > $tier['limit']) {
$ttl = $this->redis->ttl($key);
return [
'allowed' => false,
'retry_after' => $ttl,
'limit' => $tier['limit'],
'remaining' => 0
];
}
return [
'allowed' => true,
'limit' => $tier['limit'],
'remaining' => $tier['limit'] - $currentCount
];
}
}
使用注意:
- 用户识别:确保能正确识别用户(JWT Token、API Key、Session等)
- 性能优化:
- 使用Redis的Lua脚本实现原子操作
- 考虑使用Local Cache减轻Redis压力
- 分布式环境:
- Redis集群支持
- 考虑使用一致性哈希
- 错误处理:
- 返回适当的HTTP状态码(429)
- 包含Retry-After头
- 提供重试信息
根据你的具体需求选择合适的方案,Redis方案性能和灵活性最佳。