纯PHP令牌桶限流仅适用于单进程环境(如CLI、Swoole协程或PHP内置服务器),因Web多进程下存在并发写竞争与时钟漂移;需用整数微秒计算、强制floor取整规避浮点误差,并禁止批量消耗。

要让纯PHP代码在无框架、无Redis、无外部依赖的环境下实现令牌桶限流,必须绕过PHP无状态特性带来的并发写覆盖和时钟漂移问题,同时保证单请求内逻辑自洽、可复用、不污染全局变量。
理解令牌桶在纯PHP中的本质约束
PHP每次请求都是独立进程或FPM子进程,$_SESSION不能跨请求原子更新桶状态,APCu在多进程下存在写竞争,file_put_contents没有锁机制易丢数据——【所有纯PHP内存/文件方案仅适用于单进程调试或CLI脚本场景】。这意味着你写的类在Web服务器(如Nginx+PHP-FPM)中跑10个并发请求,大概率会漏放行或误拦截。
但如果你控制运行环境(比如Swoole协程、PHP内置服务器、或命令行批量调用API),就可以安全使用内存态令牌桶。
手写TokenBucket类:支持时间戳回填与速率校准
第一步:定义类结构,把容量、当前令牌数、填充速率、上次填充时间全封装为私有属性。
立即学习“PHP免费学习笔记(深入)”;
第二步:构造函数初始化时,直接设tokens = capacity,last_refill_time = microtime(true),避免首次consume时因时间差过大导致一次补满全部令牌。
第三步:refill()方法里用microtime(true)获取毫秒级时间戳,计算delta_sec = (now - last_refill_time),再按rate × delta_sec补令牌;注意要用floor()截断小数,防止浮点误差累积导致令牌数溢出。
第四步:consume()先触发refill(),再判断tokens >= 1,够就tokens--并返回true,否则返回false。这一步不能用>= $n做批量消耗,因为纯PHP没原子性,$n>1时无法保证中间不被其他请求插队。
规避浮点精度陷阱的关键修复
方法一:用整数微秒代替浮点秒计算
把rate设为“每毫秒生成多少令牌”,例如rate=0.01 → 改成rate_ms=10(即每100ms生成1个),然后用intval((microtime(true) * 1000 - $this->last_refill_time_ms) * $this->rate_ms / 1000)算新增令牌数。这样全程用整数运算,避开PHP浮点除法的0.1+0.2≠0.3问题。
方法二:强制令牌数向下取整
补完令牌后执行$this->tokens = (int)floor($this->tokens),否则$tokens可能变成9.999999999999998,后续比较if ($this->tokens >= 1)仍为true,但减1后变成8.999999999999998,下次再floor就掉点——【不floor会导致令牌数缓慢泄漏】。
CLI模式下实测可用的完整代码片段
复制以下代码保存为rate_limit.php,用php rate_limit.php运行:
require_once 'TokenBucket.php';
$bucket = new TokenBucket(5, 2); // 容量5,每秒补2个
for ($i = 0; $i if ($bucket->consume()) {
echo "✓ 请求 {$i} 通过\n";
} else {
echo "✗ 请求 {$i} 被限流\n";
}
usleep(300000); // 每300ms发一次,模拟0.33QPS
}



















