应选用 symfony/rate-limiter 的 inmemorylimiter,因其基于 hrtime() 实现无 i/o、无序列化、无锁的单机毫秒级节流,tokenbucketlimiter 需用 dateinterval 对象配置并复用实例,slidingwindowlimiter 实为计数器,真漏桶需手动建模时间差与速率关系,且仅限单 worker 原子性。

为什么不用 php-rate-limiter 这类现成包?
因为它们多数基于 Redis 或 APCu 做状态存储,引入了外部依赖和序列化开销;而你只需要在单机 PHP-FPM Worker 内做毫秒级请求节流(比如 API 网关前置限流),用 Composer 库只是为了复用可靠的时间抽象和接口契约,不是为了套个“高大上”外壳。
真正高性能的关键在于:不碰 I/O、不序列化、不加锁、状态存在 PHP 变量里(static 或对象属性),且时间戳用 hrtime(true) 而非 microtime(true) —— 后者在某些系统上会回跳或精度不足。
推荐直接使用 phpseclib/openssl 不相关,但 ramsey/uuid 也不对路;真正合用的是 symfony/rate-limiter —— 它支持内存驱动(InMemoryLimiter),且底层用 hrtime() 计算窗口,源码干净可读。
Symfony\RateLimiter\Policy\TokenBucketLimiter 怎么配才不丢令牌?
默认构造时传入的 $maxTokens 和 $interval 是静态配置,但令牌补充逻辑藏在 addTokens() 里,它依赖上一次调用时间与当前 hrtime() 差值。如果两次请求间隔极短(如并发压测),可能因浮点误差或时钟抖动导致补发 0 个令牌,进而误判“被限流”。
实操建议:
- 永远用
new TokenBucketLimiter(100, new \DateInterval('PT1S')),别用'1 second'字符串——后者触发DateInterval::__construct的隐式解析,有额外开销 - 把 limiter 实例挂到容器或 request scope 对象上,避免每次 new ——
TokenBucketLimiter内部用static存储上次时间戳,多个实例会互相覆盖 - 调用
$limiter->consume(1)前,先检查$limiter->getAvailableTokens()是否 ≥1,避免抛出RateLimitExceededException带来异常处理开销
漏桶算法在 PHP 里怎么避开“定时器缺失”的坑?
PHP 没有常驻线程,没法真跑一个后台协程持续滴水。所谓“漏桶”,本质是每次请求时根据距上次调用的时间差,计算该漏掉多少 token(即允许通过多少请求)。关键不是模拟漏水动作,而是正确建模“单位时间恒定流出速率”。
Symfony\RateLimiter\Policy\SlidingWindowLimiter 名字带 window,实际是计数器;真漏桶得自己写。核心公式:current_tokens = max(0, last_tokens - (now - last_time) * rate)。
注意三点:
- rate 单位必须统一:若窗口是 1 秒、容量 100,则 rate = 100.0;若用 ms 时间戳,rate 就得是
100.0 / 1000.0 - 务必用
hrtime(true)获取纳秒级时间戳,再除以 1e6 转 ms ——microtime(true)返回 float,在高并发下多次调用可能返回相同值,导致漏水量为 0 - 不要在
__destruct()里持久化状态:FPM worker 退出不可控,且serialize()会破坏hrtime()的精度语义
并发场景下 consume() 的原子性怎么保?
Symfony\RateLimiter 的内存实现 **不保证** 多线程/多进程原子性 —— 它只适合单个 PHP 进程内(如一个 FPM worker)的请求排队。如果你用的是 PHP 8.1+ 并启用了 opcache.preload,多个 worker 可能共享同一份类定义,但每个 worker 的 static 变量仍是隔离的。
这意味着:你不能靠它做跨 worker 的全局限流(比如限制整个服务每秒最多 1000 次调用),那必须上 Redis + Lua。但如果你的目标是“单个用户 IP 在本 worker 内每秒最多 5 次”,那就完全 OK。
验证方式很简单:
- 用
ab -n 100 -c 20压测,观察getAvailableTokens()返回值是否随时间平滑下降回升 - 在
consume()前后加var_dump(hrtime(true)),确认两次调用时间差与预期漏/补量匹配 - 别用
sleep()模拟时间流逝 —— 它会让进程挂起,hrtime()仍正常走,导致补令牌过多
真正难搞的从来不是算法本身,而是把“时间”这个物理量,在没有时钟中断、没有线程调度权的 PHP 里,用足够低的误差映射成业务规则。其他都好办。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











