PHP项目实战:如何实现本地限流计数器(附完整代码)
目录导读
为什么需要本地限流计数器?
在PHP项目中,尤其是API接口或高并发场景下,限流是保护系统不被突发流量击穿的重要手段。本地限流计数器是指不依赖外部中间件(如Redis、MySQL),直接利用PHP进程内的内存或文件系统实现请求频率控制,其典型场景包括:

- 防止单个用户恶意刷接口(如登录、短信验证码)
- 限制API每分钟调用次数(例如每个IP每秒最多10次请求)
- 保护数据库或第三方服务免受瞬时压力
相比分布式限流,本地限流部署简单、无需额外服务,适合单机应用或微服务中的单体节点限流,但需注意:多进程或集群环境下,需要改用共享内存(如APCu/SHMOP)或外部存储。
限流算法的核心原理
所有限流计数器都遵循三个要素:
| 元素 | 说明 | 示例 |
|---|---|---|
| 时间窗口 | 统计的时间范围 | 60秒、10秒 |
| 阈值 | 允许的最大请求次数 | 100次/分钟 |
| 计数存储 | 记录当前请求次数 | 文件、内存变量 |
常见算法包括:
- 固定窗口:整点重置计数,如每分钟0-60秒,缺陷:窗口边界突发流量。
- 滑动窗口:将时间窗口细分为更小的时间片,精度更高。
- 令牌桶/漏桶:更复杂的平滑控制,适合流量整形。
PHP实现本地限流,推荐使用滑动窗口算法,兼顾精度与性能。
PHP本地限流计数器的三种实现方式
1 基于文件缓存的简单计数器
适用场景:低并发、无APCu扩展的共享主机。
<?php
// SimpleFileRateLimiter.php
function rateLimitByFile($key, $maxRequests = 10, $windowSeconds = 60) {
$filePath = sys_get_temp_dir() . '/limiter_' . md5($key) . '.txt';
$now = time();
if (file_exists($filePath)) {
$data = json_decode(file_get_contents($filePath), true);
// 清除超出窗口的记录
$data = array_filter($data, function($time) use ($now, $windowSeconds) {
return $time > ($now - $windowSeconds);
});
} else {
$data = [];
}
if (count($data) >= $maxRequests) {
return false; // 限流
}
$data[] = $now;
file_put_contents($filePath, json_encode($data), LOCK_EX);
return true; // 允许
}
// 使用示例
if (!rateLimitByFile('api:user_123', 5, 10)) {
http_response_code(429);
exit('请求过于频繁,请稍后重试');
}
缺点:频繁I/O操作,高并发下容易产生文件锁竞争。
2 基于APCu内存的高性能计数器
适用场景:单机高并发、内存友好、需要毫秒级响应。
<?php
// APCuRateLimiter.php
class APCuRateLimiter {
private $keyPrefix = 'rate_limit:';
public function isAllowed($key, $maxRequests, $windowSeconds) {
$currentKey = $this->keyPrefix . $key;
$now = microtime(true);
if (!apcu_exists($currentKey)) {
apcu_store($currentKey, [$now], $windowSeconds + 1);
return true;
}
$records = apcu_fetch($currentKey);
// 清理过期记录
$records = array_filter($records, function($time) use ($now, $windowSeconds) {
return $time >= ($now - $windowSeconds);
});
if (count($records) >= $maxRequests) {
return false;
}
$records[] = $now;
apcu_store($currentKey, $records, $windowSeconds + 1);
return true;
}
}
// 使用
$limiter = new APCuRateLimiter();
if (!$limiter->isAllowed('sms:13800138000', 3, 60)) {
exit('60秒内只能发送3条短信');
}
注意:APCu默认TTL有限,需在php.ini中设置apc.ttl=0或合理赋权。
3 基于Redis的分布式限流(拓展阅读)
强调“本地”,但若需跨进程共享,Redis + Lua脚本是最优解,此方案不在本文详述,只给出核心思路:
-- Redis Lua脚本
local key = KEYS[1]
local max = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local current = redis.call('ZCARD', key)
if current < max then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1
else
return 0
end
完整代码示例:滑动窗口限流类
下面提供一个支持APCu和文件双引擎的本地限流类,并加入错误处理和单元测试示例。
<?php
class LocalRateLimiter {
private $engine;
private $config;
public function __construct($engine = 'apcu', $config = []) {
$this->engine = ($engine === 'file') ? 'file' : 'apcu';
$this->config = array_merge([
'file_path' => sys_get_temp_dir() . '/rate_limits/',
'apcu_ttl' => 300
], $config);
if ($this->engine === 'file' && !is_dir($this->config['file_path'])) {
mkdir($this->config['file_path'], 0755, true);
}
}
/**
* 尝试获取许可
* @param string $key 限流标识(如IP、用户ID)
* @param int $maxRequests 允许的最大请求数
* @param int $windowSeconds 时间窗口(秒)
* @return bool 是否允许通过
*/
public function tryAcquire($key, $maxRequests, $windowSeconds) {
if ($this->engine === 'file') {
return $this->fileAcquire($key, $maxRequests, $windowSeconds);
}
return $this->apcuAcquire($key, $maxRequests, $windowSeconds);
}
private function apcuAcquire($key, $max, $window) {
$fullKey = 'limiter:' . md5($key);
$now = microtime(true);
$records = apcu_fetch($fullKey);
if ($records === false) {
apcu_store($fullKey, [$now], $this->config['apcu_ttl']);
return true;
}
// 过滤过期记录
$validRecords = [];
foreach ($records as $time) {
if ($time >= ($now - $window)) {
$validRecords[] = $time;
}
}
if (count($validRecords) >= $max) {
return false;
}
$validRecords[] = $now;
apcu_store($fullKey, $validRecords, $this->config['apcu_ttl']);
return true;
}
private function fileAcquire($key, $max, $window) {
$filePath = $this->config['file_path'] . md5($key) . '.json';
$now = time();
$data = [];
if (file_exists($filePath)) {
$data = json_decode(file_get_contents($filePath), true) ?? [];
$data = array_filter($data, function($t) use ($now, $window) {
return $t > ($now - $window);
});
}
if (count($data) >= $max) {
return false;
}
$data[] = $now;
file_put_contents($filePath, json_encode($data), LOCK_EX);
return true;
}
}
使用示例:
$limiter = new LocalRateLimiter('apcu');
if ($limiter->tryAcquire('api:ip_192.168.1.1', 100, 60)) {
// 处理业务
} else {
header('Retry-After: 60');
http_response_code(429);
echo json_encode(['code' => 429, 'msg' => '请求频率超限']);
}
常见问题与性能调优技巧
Q1:本地限流是否安全?
- 安全性:依赖单进程内存的计数器(如普通PHP变量)在
FPM/CGI多进程模式下无效,必须用共享内存(APCu/SHMOP)或文件锁。 - 文件锁高并发:使用
flock或LOCK_EX避免数据竞争,但会降低吞吐量。
Q2:如何选择合适的存储引擎?
| 场景 | 推荐引擎 | 理由 |
|---|---|---|
| 测试/开发环境 | 文件 | 无需扩展 |
| 低并发生产 (<100qps) | 文件 + flock | 简单可靠 |
| 高并发生产 (1000+qps) | APCu / 共享内存 | 微秒级响应 |
| 集群/分布式 | Redis / Memcached | 数据一致性 |
Q3:APCu的microtime(true)精度问题?
microtime(true)返回浮点数,可精确到微秒,但如果两次请求发生在同一微秒,将导致计数偏小,可改用hrtime(true)获取纳秒级时间戳(PHP 7.3+)。
性能优化技巧:
- 减少IO次数:APCu比文件快100倍以上,生产环境优先启用APCu。
- 合理设置窗口大小:滑动窗口时间片越小,精度越高,但内存消耗越大,建议窗口大于1秒。
- 使用批量清理:不必每次都清理全量记录,可设置定时器或到达阈值时再清理。
- 配合Nginx限流:Nginx的
limit_req_zone可作为第一层防护,PHP限流作为第二层精细控制。
实战问答
问:我的PHP项目没有APCu扩展,如何实现高性能本地限流?
答:可以考虑使用Shared Memory(SHMOP)扩展或文件内存映射(mmap),但更简单的方法是:将限流数据缓存在MySQL内存表(如ENGINE=MEMORY)中,查询速度接近APCu,缺点是仍需数据库连接,但无需额外扩展。
问:固定窗口与滑动窗口哪个更适合实际项目?
答:滑动窗口更推荐,举个例子:固定窗口限速100次/分钟,如果在第一秒内请求了100次,后续59秒将完全阻塞,但真实流量可能已平摊,滑动窗口将1分钟拆分为60个1秒小窗口,能更平滑地控制流量,但若业务允许边界突刺(如每60秒重置),固定窗口实现更简单。
问:限流计数器如何防止“原子性”问题?
答:PHP原生变量非原子,需要依赖以下机制:
- 对于文件:
file_put_contents(..., LOCK_EX)加写锁 - 对于APCu:
apcu_cas()函数可原子化更新计数(但需先检查存在) - 对于Redis:使用Lua脚本保证原子性
问:如何处理限流后的“合理等待”提示?
答:建议返回HTTP 429状态码,并在响应头添加Retry-After字段(单位秒)。
header('Retry-After: 30'); // 30秒后重试
echo json_encode([
'code' => 429,
'msg' => '请求过于频繁,请30秒后重试',
'retry_after' => 30
]);
客户端(如浏览器)或API调用方可根据此头自动降级。
问:限流计数器的“冷启动”问题如何解决?
答:第一次访问时,由于没有历史记录,可以直接放行并创建记录,但需要注意:如果在短时间内大量首次请求涌入(如系统重启后),可能导致初始突发超过阈值,解决方案:
- 初期使用较宽松的阈值(例如设计容量+20%)
- 采用令牌桶算法,初始状态填充一定令牌量
通过以上代码与问答,你可以根据项目实际场景灵活选择文件或APCu实现本地限流计数器,限流不是目的,目的是让系统稳定运行,在突发流量下优雅降级,而非直接崩溃。