本文目录导读:

在 PHP 项目中实现计数器限流,通常有单机版(基于内存)和分布式版(基于 Redis)两种常见方案,以下是几种主流实现方式,从简单到生产级逐一说明。
最简单的 PHP 文件计数器(单机,不推荐生产)
这种方案适合极简的演示或临时测试,但存在并发安全问题和性能瓶颈。
<?php
class FileRateLimiter {
private $filePath;
private $limit; // 时间窗口内最大请求数
private $window; // 时间窗口(秒)
public function __construct($limit = 10, $window = 60) {
$this->filePath = sys_get_temp_dir() . '/rate_limit_counter.txt';
$this->limit = $limit;
$this->window = $window;
}
public function allowRequest() {
$data = $this->readFile();
$currentTime = time();
$ip = $this->getClientIp();
// 清理过期记录
$data = array_filter($data, function($record) use ($currentTime) {
return ($currentTime - $record['time']) < $this->window;
});
// 统计当前 IP 的请求次数
$count = 0;
foreach ($data as $record) {
if ($record['ip'] === $ip) {
$count++;
}
}
if ($count >= $this->limit) {
return false; // 被限流
}
// 添加新记录
$data[] = ['ip' => $ip, 'time' => $currentTime];
$this->writeFile($data);
return true; // 允许请求
}
private function readFile() {
if (!file_exists($this->filePath)) return [];
$content = file_get_contents($this->filePath);
return $content ? json_decode($content, true) : [];
}
private function writeFile($data) {
file_put_contents($this->filePath, json_encode($data), LOCK_EX);
}
private function getClientIp() {
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
}
// 使用示例
$limiter = new FileRateLimiter(10, 60);
if (!$limiter->allowRequest()) {
http_response_code(429);
exit('请求过于频繁,请稍后再试');
}
问题:
- 文件锁在并发高时性能极差
- 无法跨进程/跨服务器共享
- 不适合生产环境
基于 Redis 的滑动窗口计数器(推荐分布式方案)
这是最常用的生产级方案,利用 Redis 的 INCR 和 EXPIRE 配合时间窗口实现。
1 简单版(固定时间窗口 + 过期)
<?php
class RedisFixedRateLimiter {
private $redis;
private $limit;
private $window;
public function __construct($redis, $limit = 10, $window = 60) {
$this->redis = $redis;
$this->limit = $limit;
$this->window = $window;
}
public function allowRequest($key = 'default') {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$redisKey = "rate_limit:{$key}:{$ip}";
$current = $this->redis->get($redisKey);
if ($current === false) {
// 第一次请求,设置过期时间
$this->redis->setex($redisKey, $this->window, 1);
return true;
}
if ((int)$current >= $this->limit) {
return false; // 限流
}
$this->redis->incr($redisKey);
return true;
}
}
// 使用(需要先连上 Redis)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$limiter = new RedisFixedRateLimiter($redis, 10, 60);
缺点:固定窗口的边界问题(59 秒和 01 秒可能连续请求 20 次)。
2 滑动窗口版(更精确)
使用 Redis 有序集合(ZSET),按时间戳记录每次请求。
<?php
class RedisSlidingWindowLimiter {
private $redis;
private $limit;
private $window;
public function __construct($redis, $limit = 10, $window = 60) {
$this->redis = $redis;
$this->limit = $limit;
$this->window = $window;
}
public function allowRequest($key = 'default') {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$redisKey = "sliding_limit:{$key}:{$ip}";
$now = microtime(true);
$windowStart = $now - $this->window;
// Lua 脚本保证原子性
$script = <<<LUA
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
local count = redis.call('ZCARD', KEYS[1])
if count < tonumber(ARGV[3]) then
redis.call('ZADD', KEYS[1], ARGV[2], ARGV[2])
redis.call('EXPIRE', KEYS[1], ARGV[4])
return 1
else
return 0
end
LUA;
$result = $this->redis->eval(
$script,
[$redisKey, $windowStart, $now, $this->limit, $this->window],
1 // keys 数量
);
return $result === 1;
}
}
优点:
- 精确控制滑动窗口
- Lua 脚本保证原子性
- 自动过期清理
令牌桶算法(适合突发流量控制)
令牌桶允许短时间内的突发请求,但长期平均速率受控。
<?php
class RedisTokenBucketLimiter {
private $redis;
private $rate; // 每秒生成令牌数
private $capacity; // 最大桶容量
public function __construct($redis, $rate = 5, $capacity = 10) {
$this->redis = $redis;
$this->rate = $rate;
$this->capacity = $capacity;
}
public function allowRequest($key = 'default') {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$redisKey = "token_bucket:{$key}:{$ip}";
$script = <<<LUA
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call('HGETALL', key)
local tokens = capacity
local lastRefill = now
if #bucket > 0 then
tokens = tonumber(bucket[2]) or capacity
lastRefill = tonumber(bucket[4]) or now
end
-- 计算应补充的令牌
local elapsed = now - lastRefill
local refill = elapsed * rate
if refill > 0 then
tokens = math.min(capacity, tokens + refill)
end
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
redis.call('EXPIRE', key, 2) -- 短过期,避免堆积
return 1
else
return 0
end
LUA;
$result = $this->redis->eval(
$script,
[$redisKey, $this->rate, $this->capacity, microtime(true)],
1
);
return $result === 1;
}
}
Nginx + Redis 计数器(高性能方案)
PHP 运行在 Nginx 后面,可以在 Nginx 层直接用 limit_req_zone 配合 Redis 实现,性能更好。
但如果你需要在 PHP 层做更灵活的控制(例如按用户 ID 限流而非 IP),仍建议使用 PHP + Redis 方案。
生产级建议
1 封装成中间件
// Laravel/Symfony 风格中间件
class RateLimitMiddleware {
public function handle($request, $next) {
$limiter = app(RateLimiter::class);
$key = $request->ip() . ':' . $request->path();
if (!$limiter->allowRequest($key, 100, 60)) {
return response('Too Many Requests', 429);
}
return $next($request);
}
}
2 返回标准 HTTP 429
header('HTTP/1.1 429 Too Many Requests');
header('Retry-After: 60'); // 告诉客户端多久后重试
header('X-RateLimit-Limit: 100');
header('X-RateLimit-Remaining: 0');
exit('请求过于频繁');
3 异常与降级
- 当 Redis 连接失败时,建议放行请求(不做限速),避免服务完全不可用。
- 记录限流日志到独立文件或日志系统。
4 性能优化
- 使用 Redis Pipeline 批量操作(如果一次请求需要检查多个限流键)
- 设置合理的 Redis 连接池
- 考虑使用 localcache(如 APCu)配合 Redis(双缓存),减少 Redis 压力
选型建议
| 场景 | 推荐方案 |
|---|---|
| 单机小项目、演示 | 文件计数器(不推荐生产) |
| 中小型分布式项目 | Redis 滑动窗口 ZSET |
| 需要应对突发流量 | 令牌桶 |
| 精确按秒控制 | 滑动窗口或令牌桶 |
| 极致性能、Nginx 前端 | Nginx limit_req + Redis |
| 云原生、微服务 | 使用云平台 API 网关限流(如 AWS API Gateway) |
核心要点:
- 原子性操作:使用 Redis Lua 脚本或
INCR配合EXPIRE(单命令) - 滑动窗口 > 固定窗口
- 设置合理的过期时间,避免 Redis 内存泄漏
- 返回友好的错误信息和 header
如果你的项目不是特别复杂,Redis 固定窗口 + 过期时间 已经足够应对大部分场景(可以通过设置窗口为 1 秒来达到类似滑动窗口的效果)。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播