PHP用token bucket限流需协调桶容量、填充速率、时间精度三参数,依赖Redis原子操作存状态,按毫秒匀速填充并取整,响应头透出限流信息,参数需线上流量反推。

PHP里用token bucket做接口限流,核心就三件事
不是写个计数器加个Redis锁就叫限流。真正能扛住突发流量、又不误杀合法请求的,得靠token bucket——它允许短时爆发,但长期守规矩。PHP没内置实现,得自己搭骨架,关键在「桶容量」「填充速率」「时间精度」三个参数怎么协调。
Redis存桶状态,为什么不用file_put_contents或apcu_store
本地缓存(比如apcu_store)在多PHP-FPM进程下会各自为政,同一个用户请求可能被不同worker处理,桶状态不同步,直接失效;文件IO在高并发下容易阻塞甚至损坏。必须用全局、原子、带过期的存储。
-
Redis的INCRBY+EXPIRE组合能保证单次操作原子性,且支持毫秒级过期(Redis 6.0+),适合token bucket的时间滑动窗口逻辑 - 别用
SETNX手动模拟令牌消耗——竞态条件藏得深,测试时看不出,压测时突然503 - 桶的key建议包含用户标识+接口路径+时间戳(如
"rate:uid123:/api/v1/order:2024052014"),避免不同接口共用一个桶导致误限
token bucket填充逻辑写在PHP里,最容易错的是时间计算
桶不是“每秒补1个”,而是“按毫秒匀速补”。如果每次请求都重算应有令牌数,再跟当前值比大小,中间的时间差就是漏洞——两个紧挨着的请求可能都算出“还有1个令牌”,结果超发。
- 正确做法:用Redis记录上一次填充时间(
last_fill_time)和当前令牌数,每次请求先根据时间差补满,再扣减。补的量 =rate * (now - last_fill_time) / 1000(假设rate是每秒令牌数) - 注意浮点精度:
floor()取整,别用round(),否则0.9个令牌可能被进位成1个,累积误差会放大 - Redis里存时间戳用毫秒(
microtime(true) * 1000),别用time(),秒级精度在高频接口下等于没限
实际部署时,tryAcquire()返回值和HTTP状态码怎么对应
限流不是为了拦人,是为了给调用方明确信号。返回false不能只打日志,得让前端或网关知道“稍后再试”。
立即学习“PHP免费学习笔记(深入)”;
- 建议HTTP响应头带上
X-RateLimit-Remaining(剩余令牌)、X-RateLimit-Retry-After(建议重试毫秒数),方便客户端退避 - 别统一返回429——如果是登录接口被限,该返回401;如果是搜索接口被限,才用429。语义错了,网关策略就废了
- 调试阶段在Redis里用
GET手动查桶key,看tokens和last_fill_time是否随请求合理变化,比看日志快得多
真正难的不是写通逻辑,是把「填充速率」和「桶容量」调到业务可接受的抖动范围——太严,活动开始瞬间全拒;太松,DB直接被打穿。这俩数得靠线上真实流量反推,不是拍脑袋定的。



















