PHP API限流计数器实现指南:从原理到高并发实战
目录导读
为什么需要API限流?
在高并发场景下,API接口可能面临恶意攻击、异常流量或用户滥用,某个热搜新闻上线后,评论区接口可能在1秒内接收10万次请求,如果不加限制,数据库可能瞬间崩溃,导致整个服务不可用。

核心痛点:如何在不影响正常用户的前提下,精准控制单位时间内的请求量?
解决方案:PHP API限流计数器,它通过记录请求次数并与预设阈值比较,决定是否允许当前请求继续执行。
限流算法核心原理
固定窗口计数器(最简单)
- 机制:将时间划分为固定间隔(如1秒),每个窗口内记录请求数。
- 缺点:存在“突刺”问题,窗口最后一毫秒涌入999次请求,下一窗口第一毫秒涌入999次,实际2毫秒内处理了1998次请求,远超阈值。
滑动窗口计数器(更精准)
- 机制:将时间窗口细分为多个小格(如1秒分为10个100ms小格),每次请求记录当前时间,并统计过去1秒内的总请求数。
- 优点:消除窗口切换时的流量突刺,更接近真实的“单位时间限流”场景。
基于Redis的计数器实现
为什么选Redis?
- 原子操作INCR/EXPIRE解决并发竞争
- 内存数据库,单机QPS可达10万+
- 支持Lua脚本实现复杂逻辑的原子性
核心数据结构
KEY: rate_limit:{api_name}:{user_ip}
VALUE: 整数(当前请求次数)
TTL: 窗口大小(秒)
PHP代码实战:固定窗口与滑动窗口
环境要求
- PHP 7.4+
- Redis扩展(predis或phpredis)
- Composer(可选,用于依赖管理)
固定窗口计数器(演示版)
<?php
class FixedWindowLimiter {
private $redis;
private $maxRequests = 100;
private $windowSize = 60; // 秒
public function __construct($redis) {
$this->redis = $redis;
}
public function allowRequest($key) {
$current = $this->redis->incr($key);
if ($current === 1) {
// 第一次请求,设置过期时间
$this->redis->expire($key, $this->windowSize);
}
return $current <= $this->maxRequests;
}
}
// 用法示例
$limiter = new FixedWindowLimiter($redis);
if ($limiter->allowRequest('api:login:192.168.1.1')) {
// 处理业务逻辑
} else {
http_response_code(429);
echo "Too Many Requests";
}
滑动窗口计数器(进阶版)
使用Redis有序集合(Sorted Set)实现:
<?php
class SlidingWindowLimiter {
private $redis;
private $maxRequests = 100;
private $windowSize = 60;
public function __construct($redis) {
$this->redis = $redis;
}
public function allowRequest($key) {
$now = microtime(true);
$windowStart = $now - $this->windowSize;
// 移除过期数据
$this->redis->zRemRangeByScore($key, 0, $windowStart);
// 检查当前窗口请求数
$count = $this->redis->zCard($key);
if ($count >= $this->maxRequests) {
return false;
}
// 添加当前请求
$this->redis->zAdd($key, $now, uniqid());
$this->redis->expire($key, $this->windowSize + 5);
return true;
}
}
性能优化:Lua脚本原子化
将“清理+检查+添加”合并为原子操作,避免并发竞争:
-- limiter.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 count = redis.call('zCard', key)
if count >= max then
return 0
end
redis.call('zAdd', key, now, now)
redis.call('expire', key, window + 5)
return 1
PHP调用Lua脚本:
$result = $this->redis->eval(
file_get_contents('limiter.lua'),
[$key, $maxRequests, $windowSize, microtime(true)],
1
);
常见问题与优化策略
Q1:计数器的key如何设计?
回答:建议采用“业务类型+客户标识”的组合。
- IP限流:
rate_limit:{api_name}:ip:{ip} - 用户限流:
rate_limit:{api_name}:user:{userId} - API密钥限流:
rate_limit:{api_name}:apikey:{key}
Q2:Redis宕机怎么办?
回答:采用“降级策略”:
- 本地缓存备份计数(Map/Array),精度略低但可用
- 设置合理的超时时间,避免Redis恢复后出现幻读
- 核心业务建议使用Redis Cluster + 持久化
Q3:如何支持多级限流(如每秒100次+每分钟1000次)?
回答:使用多个计数器组合:
- 先检查秒级限流(更严格)
- 再检查分钟级限流(更宽松)
- 任意一级拒绝则拦截
企业级部署建议
- 配置化:将限流参数(maxRequests, windowSize)存入数据库或配置中心,支持动态调整
- 监控告警:记录被限流的请求日志,当限流触发率超过阈值时自动告警
- 响应头规范:返回429状态码时,附带
Retry-After头告知客户端等待时间 - 分布式扩展:使用Redis Cluster时,确保相同用户的请求路由到同一节点(Hash Tag)
延伸思考:如果请求量超过Redis单机瓶颈(10万QPS),可以考虑引入Nginx+Lua限流(如OpenResty),在更上层拦截流量,减轻PHP应用服务器压力。
核心总结:选择哪种计数器取决于业务容忍度,固定窗口适合对精度要求不高的场景(如部分公开API),滑动窗口适合金融交易、支付回调等需要精确控制的场景。