本文目录导读:

我来详细介绍如何在PHP中实现几种常见的限流算法:
计数器算法(固定窗口)
<?php
class CounterLimiter {
private $redis;
private $key;
private $limit;
private $window; // 时间窗口(秒)
public function __construct($redis, $key, $limit, $window = 60) {
$this->redis = $redis;
$this->key = $key;
$this->limit = $limit;
$this->window = $window;
}
public function allow() {
$current = $this->redis->get($this->key);
if ($current === false) {
$this->redis->setex($this->key, $this->window, 1);
return true;
}
if ($current >= $this->limit) {
return false;
}
$this->redis->incr($this->key);
return true;
}
}
// 使用示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$limiter = new CounterLimiter($redis, 'api_limit', 100, 60);
if ($limiter->allow()) {
echo "请求允许";
} else {
echo "请求被限流";
}
滑动窗口算法
<?php
class SlidingWindowLimiter {
private $redis;
private $key;
private $limit;
private $window;
public function __construct($redis, $key, $limit, $window = 60) {
$this->redis = $redis;
$this->key = $key;
$this->limit = $limit;
$this->window = $window;
}
public function allow() {
$now = microtime(true);
$key = $this->key . ':sliding';
// 添加当前时间戳
$this->redis->zAdd($key, $now, $now . '_' . uniqid());
// 移除窗口之外的数据
$this->redis->zRemRangeByScore($key, 0, $now - $this->window);
// 设置过期时间
$this->redis->expire($key, $this->window + 1);
// 获取窗口内的请求数
$count = $this->redis->zCard($key);
return $count <= $this->limit;
}
}
令牌桶算法
<?php
class TokenBucketLimiter {
private $redis;
private $key;
private $capacity; // 桶容量
private $rate; // 令牌生成速率(个/秒)
public function __construct($redis, $key, $capacity, $rate) {
$this->redis = $redis;
$this->key = $key;
$this->capacity = $capacity;
$this->rate = $rate;
}
public function allow() {
$key = $this->key . ':bucket';
$tokensKey = $key . ':tokens';
$timeKey = $key . ':time';
// 使用Lua脚本保证原子性
$script = <<<LUA
local tokens = redis.call('GET', KEYS[1])
local last_time = redis.call('GET', KEYS[2])
if not tokens then
tokens = ARGV[1]
last_time = ARGV[2]
else
local now = tonumber(ARGV[2])
local elapsed = now - tonumber(last_time)
local new_tokens = tonumber(tokens) + (elapsed * tonumber(ARGV[3]))
if new_tokens > tonumber(ARGV[1]) then
tokens = ARGV[1]
else
tokens = tostring(new_tokens)
end
last_time = ARGV[2]
end
if tonumber(tokens) >= 1 then
redis.call('SET', KEYS[1], tonumber(tokens) - 1)
redis.call('SET', KEYS[2], last_time)
return 1
else
redis.call('SET', KEYS[1], tokens)
redis.call('SET', KEYS[2], last_time)
return 0
end
LUA;
$result = $this->redis->eval(
$script,
[$tokensKey, $timeKey],
2,
$this->capacity,
microtime(true),
$this->rate
);
return $result == 1;
}
}
漏桶算法
<?php
class LeakyBucketLimiter {
private $redis;
private $key;
private $capacity; // 桶容量
private $leakRate; // 漏水速率(个/秒)
public function __construct($redis, $key, $capacity, $leakRate) {
$this->redis = $redis;
$this->key = $key;
$this->capacity = $capacity;
$this->leakRate = $leakRate;
}
public function allow() {
$key = $this->key . ':leaky';
$waterKey = $key . ':water';
$timeKey = $key . ':time';
// 使用Lua脚本保证原子性
$script = <<<LUA
local water = redis.call('GET', KEYS[1])
local last_time = redis.call('GET', KEYS[2])
if not water then
water = 0
last_time = ARGV[2]
else
local now = tonumber(ARGV[2])
local elapsed = now - tonumber(last_time)
local leaked = elapsed * tonumber(ARGV[3])
water = math.max(0, tonumber(water) - leaked)
last_time = ARGV[2]
end
if tonumber(water) < tonumber(ARGV[1]) then
redis.call('SET', KEYS[1], tonumber(water) + 1)
redis.call('SET', KEYS[2], last_time)
return 1
else
redis.call('SET', KEYS[1], water)
redis.call('SET', KEYS[2], last_time)
return 0
end
LUA;
$result = $this->redis->eval(
$script,
[$waterKey, $timeKey],
2,
$this->capacity,
microtime(true),
$this->leakRate
);
return $result == 1;
}
}
分布式限流中间件
<?php
class RateLimitMiddleware {
private $limiter;
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
// IP限流
public function ipLimiter($ip, $limit = 100, $window = 60) {
$this->limiter = new SlidingWindowLimiter(
$this->redis,
"rate_limit:ip:{$ip}",
$limit,
$window
);
return $this->limiter;
}
// 用户限流
public function userLimiter($userId, $limit = 50, $window = 60) {
$this->limiter = new TokenBucketLimiter(
$this->redis,
"rate_limit:user:{$userId}",
$limit,
$limit / $window
);
return $this->limiter;
}
// API限流
public function apiLimiter($apiPath, $limit = 1000, $window = 3600) {
$this->limiter = new CounterLimiter(
$this->redis,
"rate_limit:api:{$apiPath}",
$limit,
$window
);
return $this->limiter;
}
}
// 使用示例
class ApiController {
private $rateLimitMiddleware;
public function __construct() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$this->rateLimitMiddleware = new RateLimitMiddleware($redis);
}
public function handleRequest() {
$ip = $_SERVER['REMOTE_ADDR'];
// 检查IP限流
$ipLimiter = $this->rateLimitMiddleware->ipLimiter($ip, 100, 60);
if (!$ipLimiter->allow()) {
http_response_code(429);
echo json_encode(['error' => 'Too Many Requests']);
return;
}
// 处理业务逻辑
// ...
}
}
基于文件系统的简单限流
<?php
class FileBasedLimiter {
private $logFile;
private $limit;
private $window;
public function __construct($logFile, $limit = 100, $window = 60) {
$this->logFile = $logFile;
$this->limit = $limit;
$this->window = $window;
}
public function allow() {
$now = time();
$logs = $this->readLogs();
// 清理过期日志
$logs = array_filter($logs, function($timestamp) use ($now) {
return ($now - $timestamp) <= $this->window;
});
// 检查是否超过限制
if (count($logs) >= $this->limit) {
return false;
}
// 添加当前请求
$logs[] = $now;
$this->writeLogs($logs);
return true;
}
private function readLogs() {
if (!file_exists($this->logFile)) {
return [];
}
$content = file_get_contents($this->logFile);
return $content ? explode(',', $content) : [];
}
private function writeLogs($logs) {
// 只保留最近时间窗口内的日志
$now = time();
$logs = array_filter($logs, function($timestamp) use ($now) {
return ($now - $timestamp) <= $this->window;
});
file_put_contents($this->logFile, implode(',', $logs));
}
}
使用建议
-
选择合适的算法:
- 简单场景:计数器算法
- 精确控制:滑动窗口算法
- 允许突发流量:令牌桶算法
- 平滑处理:漏桶算法
-
性能优化:
- 使用Redis Lua脚本保证原子性
- 合理设置缓存过期时间
- 考虑使用本地缓存减少Redis请求
-
实际应用:
- 结合IP、用户、API等多维度限流
- 返回适当的HTTP状态码(429)
- 提供限流信息头(X-RateLimit-*)
这些实现可以根据具体需求进行调整和优化。