PHP接口限流怎么做

wen PHP项目 3

本文目录导读:

PHP接口限流怎么做

  1. 文章标题:PHP接口限流实战指南:从令牌桶到Redis滑动窗口的完整实现
  2. 目录导读
  3. 为什么接口必须限流?
  4. 限流算法选型:谁最适合PHP?
  5. PHP代码实现核心方案(基于Redis)
  6. 分布式场景下的高阶技巧
  7. 防误杀与降级策略
  8. 性能与测试:防坑指南
  9. FAQ快问快答

PHP接口限流实战指南:从令牌桶到Redis滑动窗口的完整实现


目录导读

  1. 为什么接口必须限流? – 业务风险与架构韧性
  2. 限流算法选型 – 计数器、滑动窗口、令牌桶、漏桶的对比
  3. PHP代码实现核心方案 – 基于Redis的原子操作+平滑限流
  4. 分布式场景下的高阶技巧 – 一致性哈希与Lua脚本
  5. 防误杀与降级策略 – 动态配额与白名单机制
  6. 性能与测试 – 基准测试、压测报告及常见坑
  7. FAQ快问快答 – 10个高频问题精解

为什么接口必须限流?

现代API面临爬虫、刷单、恶意攻击和突发流量,未限流的接口会导致数据库连接耗尽、响应延迟飙升,甚至雪崩。限流本质是牺牲部分请求的即时性,换取整体系统的可用性,在PHP场景下,由于传统PHP-FPM的无状态特性,限流必须依赖外部存储(如Redis),而非进程内变量。


限流算法选型:谁最适合PHP?

算法 优点 缺点 PHP适用性
固定窗口 实现简单(INCR+EXPIRE 临界突发(窗口边界双倍流量)
滑动窗口 平滑,精准控制 Redis内存占用高(需ZSET)
令牌桶 允许突发流量,速率稳定 需维护令牌补充逻辑
漏桶 强制平滑,绝对稳定 无法应对突发

首选令牌桶(允许秒杀场景的短时突发),次选滑动窗口(对时间精度要求苛刻时)。


PHP代码实现核心方案(基于Redis)

1 最简固定窗口(入门版)

// 每分钟限100次
$key = 'rate_limit:' . $userId . ':' . date('Y-m-d-H-i');
$current = Redis::incr($key);
if ($current === 1) {
    Redis::expire($key, 60);
}
if ($current > 100) {
    throw new \Exception('Too Many Requests', 429);
}

缺陷:第59秒和第61秒各100次,总请求可达200次。

2 真正的令牌桶(生产级)

使用Lua脚本保证原子性,避免竞态条件:

-- KEYS[1]: 令牌桶key, ARGV[1]: 容量, ARGV[2]: 每秒速率, ARGV[3]: 当前时间戳
local bucket = redis.call('HMGET', KEYS[1], 'last_refill', 'tokens')
local lastRefill = tonumber(bucket[1]) or tonumber(ARGV[3])
local tokens = tonumber(bucket[2]) or tonumber(ARGV[1])
local delta = math.max(0, tonumber(ARGV[3]) - lastRefill)
tokens = math.min(tonumber(ARGV[1]), tokens + delta * tonumber(ARGV[2]))
redis.call('HMSET', KEYS[1], 'last_refill', ARGV[3], 'tokens', tokens)
if tokens < 1 then
    return 0 -- 拒绝
else
    redis.call('HINCRBY', KEYS[1], 'tokens', -1)
    return 1 -- 允许
end

PHP调用:

$result = Redis::eval($luaScript, 1, "user:'.$userId.'", $capacity, $rate, time());
if (!$result) { http_response_code(429); exit('Slow Down'); }

3 滑动窗口(ZSET精确版本)

$key = "sliding:" . $userId;
$now = microtime(true);
Redis::zadd($key, $now, $requestId);
Redis::zremrangebyscore($key, 0, $now - 60); // 移除60秒前的记录
$count = Redis::zcard($key);
if ($count > 50) { 
    Redis::zrem($key, $requestId); // 回滚本次
    exit('限流'); 
}
Redis::expire($key, 60);

注意:单个用户每分钟50次,内存开销极小(每个请求约32字节)。


分布式场景下的高阶技巧

问题:负载均衡下PHP多实例,Redis单点瓶颈。
解决方案

  • 内存+Redis二级缓存:单实例用APCu本地计数,超过80%阈值再同步Redis,减少网络IO。
  • Lua脚本防竞态:务必使用EVAL执行原子操作,不要用WATCH重试,避免网络往返。

生产级Lua脚本(支持漏斗+令牌桶混合)

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local interval = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - interval)
local count = redis.call('ZCARD', key)
if count >= limit then
    return 0
else
    redis.call('ZADD', key, now, now .. '-' .. math.random(100000))
    redis.call('PEXPIRE', key, interval * 2)
    return 1
end

防误杀与降级策略

  1. 白名单机制:内网IP/合作方商不限制,通过in_array(ip, $whitelist)跳过。
  2. 动态配额:根据用户等级调整$capacity,VIP用户限流阈值提升10倍。
  3. 降级熔断:当Redis不可用时,改用本地APCu(进程级)限流,牺牲精确度保可用性:
    if (apcu_inc($key) > $fallbackLimit) { exit('fallback limit'); }

性能与测试:防坑指南

压测结果(Intel i7 + PHP7.4 + Redis6)

  • 固定窗口:单机 8.5万 QPS
  • Lua令牌桶:单机 4.2万 QPS
  • ZSET滑动窗口:单机 1.8万 QPS(因ZADD时间复杂度O(logN))

常见坑

  • 不要用file_put_contents记录日志,会锁IO卡死接口。
  • Redis连接用pconnect长连接,但注意phpredis扩展的坑(eval返回结果需严格处理)。
  • 测试时用ab -n 10000 -c 100 http://yourdomain/api/test,观察429状态码比例。

FAQ快问快答

Q1:限流为什么要用令牌桶而不是计数器?
A:计数器无法处理突发流量,令牌桶允许一次请求3个令牌来应对秒杀,且平均速率恒定。

Q2:用户维度限流和IP维度限流怎么结合?
A:直接用复合键IP:userId,或者先查IP再查用户,两层判断。

Q3:Nginx层限流与PHP层限流选哪个?
A:Nginx限流(limit_req_zone)应对DDoS,PHP层处理业务逻辑(如按用户ID限流),可同时使用。

Q4:Lua脚本有性能瓶颈吗?
A:Redis执行Lua时是原子且阻塞的,但脚本耗时极短(<1ms),远低于网络开销。

Q5:如何避免误封?
A:设置3倍惩罚后自动解封,例如超限10%封禁1分钟,之后滑动窗口自动恢复。

Q6:限流后返回什么HTTP状态码?
A:429 + Retry-After头,便于客户端主动等待。

Q7:Redis内存不够怎么办?
A:用unixsocket连接减少TCP开销,对ZSET设置TTL + maxmemory-policy allkeys-lru

Q8:PHP-FPM的$_SERVER获取IP是不可靠的。
A:用HTTP_X_FORWARDED_FOR第一个值,但要防止伪造,可结合Nginx配置real_ip_header

Q9:限流只适合写接口吗?
A:读接口也要限流,防止恶意爬虫抓取,建议读接口QPS控制在正常用户10倍以下。

Q10:开源方案推荐吗?
A:轻量需求用vyuldashev/laravel-rate-limiter,复杂场景直接用Redis API自研。


PHP限流不是简单的INCR+EXPIRE,而是需要结合业务、算法、存储的综合性设计,推荐先跑通Lua令牌桶,再逐步优化为混合模式,记住核心原则——限流必须做在业务逻辑之前,且独立于主流程,否则故障时反而拖垮系统,您可以根据自身系统负载,从本文代码直接改造上生产,欢迎交流压测数据。

抱歉,评论功能暂时关闭!