站里几个接口前阵子被高频请求打得很惨,日志里全是同一批 IP 在轮询。后来花了一下午做限流,这里分享几个不用上 Redis、单文件就能跑的土办法,适合小站和自用接口。
1. IP + 时间窗口计数
最朴素的思路是记录每个 IP 在固定时间窗口内请求了几次,超过阈值就拒绝。用文件做计数器就行,但并发一定要加锁,不然计数会互相覆盖。
2. 按登录用户限流
很多操作其实应该按用户而不是按 IP 来限,比如签到、发帖、转积分。按 uid 计数更准,因为换代理和换 IP 都很容易,换账号成本更高。
3. 接口分级配置
不要让所有接口共用同一个阈值。公开接口和登录后接口分开设置,正常用户的体验会稳很多。
下面是一个可以直接改出来的最小版本:
<?php
class SimpleLimiter {
private $dir;
private $max;
private $window;
public function __construct($dir, $max = 60, $window = 60) {
$this->dir = $dir;
$this->max = $max;
$this->window = $window;
is_dir($dir) || mkdir($dir, 0777, true);
}
public function check($key) {
$file = $this->dir . '/' . md5($key) . '.json';
$fp = fopen($file, 'c+');
flock($fp, LOCK_EX);
$data = json_decode((string)stream_get_contents($fp), true) ?: ['count' => 0, 'start' => time()];
if (time() - $data['start'] > $this->window) {
$data = ['count' => 0, 'start' => time()];
}
$data['count']++;
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($data));
flock($fp, LOCK_UN);
fclose($fp);
return $data['count'] <= $this->max;
}
}
$limiter = new SimpleLimiter(sys_get_temp_dir() . '/api_limit');
if (!$limiter->check($_SERVER['REMOTE_ADDR'])) {
http_response_code(429);
header('Retry-After: 60');
exit('请求太频繁了,休息一下再来');
}
if (!empty($_SESSION['uid']) && !$limiter->check('u' . $_SESSION['uid'])) {
http_response_code(429);
exit('操作太快了,慢一点');
}
几个实际踩过的坑
- 一开始没加 flock,高并发下文件计数会互相覆盖,限流会穿透。
- 窗口时间别设太短,60 秒比较顺手,太短容易误伤正常访问。
- 被限流时返回 429,前端和日志都更好判断。
- 文件做计数适合小站,流量上来后就切到 Redis 的 INCR + EXPIRE。
这种写法够应付轻量站点的日常防刷,到了更高并发场景,思路可以平滑迁移到 Redis 或网关层。主题如果想继续展开,我会下一篇补一版 Nginx 和 PHP 双层限流的实战写法。