1. 当API没有限流时会发生什么?
上周排查一个线上故障时,发现某个PHP接口的响应时间从平时的200ms飙升到8秒以上。查看日志才发现,这个处理订单状态的API接口被某个爬虫以每秒300次的频率疯狂调用。由于没有做任何限流措施,数据库连接池直接被撑爆,导致正常用户请求大量超时——这就是典型的无限制API引发的血案。
在PHP开发的Web应用中,API接口如果没有合理的流量控制,至少会引发以下五类严重问题:
-
资源耗尽型崩溃:每个API调用都会消耗服务器资源(CPU、内存、数据库连接等)。当突发流量超过系统承载能力时,会出现:
- 数据库连接池耗尽(报错"Too many connections")
- 文件描述符耗尽(无法建立新连接)
- 内存溢出(PHP进程被OOM Killer终止)
-
业务逻辑漏洞被放大:比如:
- 短信验证码接口被刷导致营销成本激增
- 抽奖接口被脚本重复调用破坏活动公平性
- 竞品通过高频调用拖慢你的服务响应速度
-
安全防线失效:
- 暴力破解攻击(登录接口无限尝试)
- SQL注入等攻击可以不受阻碍地反复尝试
- 敏感数据接口被遍历爬取
-
服务质量雪崩:
- 正常用户请求被恶意流量淹没
- 响应时间呈指数级增长(如下图)
- 自动扩容触发导致云服务费用激增
-
上下游连锁反应:
- 数据库慢查询堆积
- 缓存被击穿
- 第三方API调用超限额(如支付接口)
真实案例:某电商促销时,由于未对商品详情API限流,爬虫在1小时内发起200万次请求,导致MySQL主库CPU持续100%,最终全站服务不可用长达2小时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. PHP API限流的六大核心方案
2.1 基础计数器模式(适合中小流量)
这是最简单的限流实现,用Redis的INCR命令即可完成:
php复制$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$api_key = 'user:' . $user_id;
$current = $redis->incr($api_key);
if ($current === 1) {
$redis->expire($api_key, 60); // 首次设置过期时间
}
if ($current > 100) {
http_response_code(429);
die('请求太频繁');
}
关键参数计算:
- 限流阈值 = 系统最大QPS × 安全系数(建议0.7)
- 时间窗口 = 业务容忍的突发时长(通常60秒)
2.2 滑动窗口算法(精准控制)
基础计数器会有时间窗口边界突增问题,改进方案:
php复制$now = microtime(true);
$window_size = 60; // 秒
$max_requests = 100;
$redis->zRemRangeByScore('api_requests', 0, $now - $window_size);
$requests = $redis->zCard('api_requests');
if ($requests < $max_requests) {
$redis->zAdd('api_requests', $now, uniqid());
} else {
header('Retry-After: ' . $window_size);
die('请求超限');
}
2.3 漏桶算法(平滑流量)
适合需要绝对流量均匀的场景:
php复制$capacity = 100; // 桶容量
$leak_rate = 1; // 每秒处理数
$last_time = $redis->get('last_leak');
$water = $redis->get('water_level') ?: 0;
$now = time();
$elapsed = $now - $last_time;
$water = max(0, $water - $elapsed * $leak_rate);
if ($water < $capacity) {
$water++;
$redis->mset(['last_leak' => $now, 'water_level' => $water]);
} else {
header('X-RateLimit-Retry-After: ' . ceil(($water - $capacity)/$leak_rate));
die('系统繁忙');
}
2.4 令牌桶算法(允许突发)
这是最常用的生产级方案,使用PHP的Redis扩展实现:
php复制$key = 'api_token_bucket:' . $client_ip;
$rate = 10; // 每秒生成10个令牌
$capacity = 100; // 桶容量
$now = microtime(true);
$tokens = $redis->hGetAll($key);
if (empty($tokens)) {
$tokens = ['tokens' => $capacity, 'timestamp' => $now];
} else {
$delta = $rate * ($now - $tokens['timestamp']);
$tokens['tokens'] = min($capacity, $tokens['tokens'] + $delta);
$tokens['timestamp'] = $now;
}
if ($tokens['tokens'] >= 1) {
$tokens['tokens'] -= 1;
$redis->hMSet($key, $tokens);
} else {
$wait = ceil((1 - $tokens['tokens']) / $rate * 1000);
header('Retry-After: ' . $wait);
die(json_encode(['error' => '请求超限']));
}
2.5 Nginx层限流(高性能方案)
在Nginx配置中直接限流(适合全局控制):
nginx复制limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location ~ ^/api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
}
}
参数说明:
10m:共享内存区大小(1MB可存约1.6万个状态)rate=10r/s:每秒10个请求burst=20:允许突发20个请求
2.6 分布式限流方案
对于集群环境,需要使用分布式协调:
php复制// 使用Redis+Lua保证原子性
$script = <<<LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local current = redis.call('GET', key)
if current and tonumber(current) >= limit then
return 0
else
redis.call('INCR', key)
if current == nil then
redis.call('EXPIRE', key, expire)
end
return 1
end
LUA;
$allowed = $redis->eval($script, ['api_limit:user123', 100, 60], 1);
if (!$allowed) {
die('集群请求超限');
}
3. 生产环境实战要点
3.1 多维度限流策略组合
实际项目需要分层防御:
| 层级 | 限流维度 | 工具 | 阈值设置 |
|---|---|---|---|
| 全局 | IP地址 | Nginx | 1000次/分钟 |
| 业务 | 用户ID | Redis | 50次/10秒 |
| 功能 | 手机号 | MySQL | 5次/分钟 |
| 敏感 | 支付接口 | 令牌桶 | 10次/秒 |
3.2 动态调整策略
通过实时监控自动调节:
php复制// 根据CPU负载动态调整
$load = sys_getloadavg()[0];
$max_rate = $load > 5 ? 50 : 100;
$redis->set('current_max_rate', $max_rate);
3.3 优雅降级方案
当触发限流时应该:
- 返回标准HTTP 429状态码
- 响应头包含
Retry-After时间 - 日志记录异常请求详情
- 触发告警通知(但需防告警风暴)
php复制http_response_code(429);
header('Content-Type: application/json');
header('Retry-After: 60');
echo json_encode([
'error' => 'too_many_requests',
'detail' => '当前请求频率过高',
'retry_after' => 60
]);
// 异步记录到日志系统
go(function() use ($request) {
$logger = new KafkaLogger();
$logger->send($request);
});
4. 常见问题与排查技巧
4.1 Redis超时问题
现象:限流检查导致API响应变慢
解决方案:
php复制$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 0.1); // 100ms超时
$redis->setOption(Redis::OPT_READ_TIMEOUT, 0.1);
4.2 限流规则失效
可能原因:
- Redis持久化导致计数器丢失
- 服务器时间不同步
- Nginx配置未重载
检查清单:
- 执行
redis-cli info persistence确认rdb/aof状态 - 用
ntpdate -q检查时间同步 - 执行
nginx -t && nginx -s reload
4.3 误杀正常流量
处理方案:
- 建立白名单机制:
php复制$whitelist = ['192.168.1.100', '10.0.0.0/8'];
if (in_array($_SERVER['REMOTE_ADDR'], $whitelist)) {
return true;
}
- 实施分级限流:
php复制$user_level = getUserLevel(); // 从数据库读取用户等级
$limits = [
'vip' => 1000,
'normal' => 100,
'free' => 10
];
4.4 分布式环境一致性
使用RedLock算法解决:
php复制$redlock = new RedLock([
'127.0.0.1:6379',
'127.0.0.1:6380',
'127.0.0.1:6381'
]);
$lock = $redlock->lock('global_rate_limit', 1000);
if ($lock) {
// 执行限流检查
$redlock->unlock($lock);
}
5. 性能优化技巧
-
连接复用:使用连接池替代短连接
php复制$pool = new RedisPool('127.0.0.1', 6379); $redis = $pool->get(); // ...业务代码... $pool->put($redis); -
批量处理:合并多个限流检查
php复制$pipe = $redis->pipeline(); $pipe->incr('user:1000'); $pipe->incr('ip:192.168.1.1'); $results = $pipe->exec(); -
本地缓存:减少Redis访问
php复制$apcu_available = function_exists('apcu_fetch'); if ($apcu_available) { $count = apcu_inc('local_counter'); if ($count > 10) { // 触发全局检查 } } -
异步记录:非阻塞日志
php复制Swoole\Runtime::enableCoroutine(); go(function() { file_put_contents('/tmp/limit.log', date('[Y-m-d H:i:s]')." 触发限流\n", FILE_APPEND); });
在PHP 8.1+环境下,还可以使用纤程进一步提升性能:
php复制$scheduler = new FiberScheduler;
$scheduler->run(
fn() => Fiber::suspend(rateLimitCheck())
);
