本文目录导读:

- 核心原理:滑动窗口 + 动态阈值
- 方案一:基于时间段的静态高峰规则(最推荐,实现简单)
- 方案二:自适应动态阈值(基于实时QPS)
- 方案三:基于等待队列 + 优先级(业务高峰友好型)
- 方案四:全局限流 + 用户分级(终极方案)
- 关键实施建议
针对PHP项目的高峰时段限流,核心思路是动态调整阈值,不能只设置一个固定的“每分钟100次”的规则,而是要在流量高峰(如秒杀、大促、整点)时自动切换为更严格的规则(如每分钟20次或直接拒绝)。
以下是一套完整的高峰加强限制方案,从简单到复杂,分为三个层级:
核心原理:滑动窗口 + 动态阈值
- 滑动窗口:避免固定时间窗口(如整点)的“流量毛刺”问题,使用Redis的有序集合或Lua脚本实现。
- 动态阈值:根据当前流量状态(QPS、CPU、队列深度)决定使用哪套规则。
基于时间段的静态高峰规则(最推荐,实现简单)
适用场景:已知固定高峰时间(如每天 10:00-12:00,20:00-22:00)。
实现步骤:
- 定义多套限流配置:
normal:100次/秒peak:20次/秒
- 判断当前时段:
- 如果在
peak时间范围内,使用严格配置。
- 如果在
- 限流执行:使用Redis + Lua原子计数。
PHP代码示例(使用Redis + 滑动窗口):
class RateLimiter {
private $redis;
private $prefix = 'ratelimit:sliding:';
public function isAllowed($userId, $action = 'api') {
$period = $this->getCurrentPeriod(); // 返回 'peak' 或 'normal'
$rules = $this->getRules($period, $action);
$key = $this->prefix . $userId . ':' . $action;
$now = microtime(true);
$windowStart = $now - $rules['window']; // 1秒
// Lua脚本:移除窗口外数据,统计窗口内请求
$lua = <<<LUA
local key = KEYS[1]
local now = tonumber(ARGV[1])
local windowStart = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- 移除过期数据
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
-- 统计当前窗口请求数
local count = redis.call('ZCARD', key)
-- 判断是否超限
if count >= limit then
return 0 -- 拒绝
end
-- 添加当前请求
redis.call('ZADD', key, now, now .. ':' .. math.random())
-- 设置过期时间,防止内存泄漏
redis.call('EXPIRE', key, math.ceil($rules['window']))
return 1 -- 允许
LUA;
$result = $this->redis->eval($lua, [$key, $now, $windowStart, $rules['limit']], 1);
return $result == 1;
}
private function getCurrentPeriod(): string {
$hour = (int)date('H');
$minute = (int)date('i');
// 定义高峰时段:10:00-12:00 和 20:00-22:00
if (($hour == 10 && $minute >= 0) || ($hour == 11) || ($hour == 12 && $minute == 0)) {
return 'peak';
}
if (($hour == 20 && $minute >= 0) || ($hour == 21) || ($hour == 22 && $minute == 0)) {
return 'peak';
}
return 'normal';
}
private function getRules(string $period, string $action): array {
// 可根据 $action 细分规则
$rules = [
'peak' => ['limit' => 20, 'window' => 1], // 高峰每秒20次
'normal' => ['limit' => 100, 'window' => 1], // 平峰每秒100次
];
return $rules[$period] ?? $rules['normal'];
}
}
// 使用
$limiter = new RateLimiter();
if (!$limiter->isAllowed($userId)) {
http_response_code(429);
die(json_encode(['error' => 'Too Many Requests']));
}
自适应动态阈值(基于实时QPS)
适用场景:高峰时间不确定(如突发流量、营销活动),需要系统自动识别并加强。
核心逻辑:
- 监控当前QPS(每秒请求数)。
- 如果QPS超过预设的警戒线(如80% * 服务器处理能力),则自动将限流阈值降低50%。
- 使用PID控制器或简单比例控制:
current_limit = target_limit * (1 - (current_qps / max_qps - 0.8))
示例逻辑(伪代码):
class AdaptiveRateLimiter {
private function getCurrentQps(): float {
// 从Redis计数器读取最近1秒的请求数
return $this->redis->get('qps_counter');
}
private function getAdjustedLimit(): int {
$baseLimit = 200; // 基础阈值
$maxQps = 250; // 服务最大处理能力(需压测得出)
$currentQps = $this->getCurrentQps();
$usageRate = $currentQps / $maxQps;
if ($usageRate > 1.0) {
return intval($baseLimit * 0.1); // 严重超载,急剧收紧
}
if ($usageRate > 0.8) {
// 超过80%负载,按比例收紧
return intval($baseLimit * (1 - ($usageRate - 0.8) * 2));
}
return $baseLimit; // 正常负载
}
}
注意:此方案需要配合熔断机制,当QPS异常高时,直接拒绝所有请求,保护数据库。
基于等待队列 + 优先级(业务高峰友好型)
适用场景:不希望直接拒绝普通用户,而是让它们排队等待,高峰时延长等待时间。
实现思路:
- 使用Redis List或Sorted Set作为队列。
- 正常时:队列容量100,等待时间<1秒。
- 高峰时:队列容量降低到20,等待时间增加到5秒,超时失败。
// 判断是否需要丢进队列
$queueKey = 'queue:' . $action;
$queueLen = $this->redis->LLEN($queueKey);
// 高峰规则:队列长度超过10就拒绝
$queueLimit = $this->isPeakTime() ? 10 : 100;
if ($queueLen >= $queueLimit) {
// 拒绝或返回重试
return false;
}
// 入队,等待处理
$this->redis->RPUSH($queueKey, $requestId);
// 设置超时时间
$this->redis->EXPIRE($queueKey, $this->isPeakTime() ? 5 : 60);
全局限流 + 用户分级(终极方案)
高峰时,不仅限流,还要按用户优先级分配资源。
- VIP用户:高峰期限流阈值不变(100次/秒)。
- 普通用户:高峰期限流阈值降低到10次/秒。
- 游客:高峰期直接限流到1次/秒或拒绝。
实现:在RateLimiter::isAllowed()中增加$userLevel参数,携带不同的rules数组。
关键实施建议
- 双阈值熔断:设置两个阈值,一个为警告阈值(触发日志、报警),一个为拒绝阈值(触发限流),高峰时段直接使用拒绝阈值。
- 不要在高峰时再去查数据库:限流配置应该缓存到Redis或本地内存(如APCu)。
- 配合Sentinel:如果使用微服务,可以集成Sentinel(阿里开源,有PHP扩展)。
- 削峰填谷:对于异步任务(如发短信、上传图片),使用消息队列(如RabbitMQ)平滑处理,高峰时降低队列消费速率。
- 测试与压测:必须通过JMeter或wrk压测,找到服务器的最大安全QPS和最大安全连接数,以此为基础设置高峰阈值。
- 最简单最可靠:方案一(固定时间切换) + 熔断。
- 需要灵活性:方案一 + 方案二(静态时间+自适应调整)。
- 面向大流量:方案四(用户分级) + 方案三(队列)。
核心原则:永远不要让服务完全崩溃,宁可丢掉请求,不可拖垮数据库。