文生图限流必须用令牌桶而非固定窗口或漏桶,因其支持突发流量且适应长耗时、不稳定成功率场景;Redis Lua脚本原子实现补令牌、判断、扣减;推荐uid/IP/全局三级键组合限流。

PHP 调用文生图(如 Stable Diffusion API、DALL·E 封装接口等)必须限频,否则极大概率被目标服务封 IP 或返回 429 Too Many Requests。单纯用 sleep() 或固定计数器不行——它不抗并发、不跨进程、不保原子性,线上一压就崩。
为什么文生图场景特别需要令牌桶,而不是固定窗口?
文生图请求耗时长(常 2–10 秒)、成功率不稳定(可能重试)、且用户行为有明显突发性(比如运营活动推图、批量生成封面)。固定窗口(如“每分钟最多 60 次”)会在窗口切换时允许双倍请求;漏桶又太死板,无法应对合理突发。令牌桶的“容量 + 速率”组合刚好匹配:
-
$capacity = 10:允许用户一口气提交 10 张图任务(比如批量生成),不卡顿 -
$rate = 2(token/s):长期平均 2 张/秒,即每分钟稳定放行约 120 张,防持续刷量 - 空闲时令牌自动积累,用户隔 30 秒再调用,桶里已有 60 个 token,可立刻连发
Redis 实现令牌桶的关键 Lua 脚本必须原子执行
PHP 自己算时间差再读写 Redis 是竞态高发区。必须用 Lua 把“补令牌 + 判+扣”三步锁死在服务端。以下是最简可用脚本(存为 token_bucket.lua):
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local last_refill = tonumber(redis.call('hget', key, 'last_refill') or '0')
local tokens = tonumber(redis.call('hget', key, 'tokens') or tostring(capacity))
<p>-- 补令牌:按时间差计算,不超过 capacity
local elapsed = now - last_refill
if elapsed > 0 then
local add = math.floor(elapsed * rate)
tokens = math.min(capacity, tokens + add)
end</p><p>-- 尝试扣 1 个
if tokens >= 1 then
redis.call('hset', key, 'tokens', tokens - 1)
redis.call('hset', key, 'last_refill', now)
return 1
else
return 0
endPHP 调用时只需传入唯一键(如 rate:uid:123:api:txt2img)、容量、速率和当前时间戳:$redis->eval($lua, [$key, $capacity, $rate, $now], 1)。返回 1 表示放行,0 表示拒绝。
立即学习“PHP免费学习笔记(深入)”;
PHP 中怎么设计限流维度和键名?
文生图接口的限流粒度不能只按用户 ID,否则一个恶意用户用多个账号就绕过;也不能只按 IP,内网或 NAT 环境下会误伤。推荐三级组合键:
- 主维度:
rate:uid:{user_id}:txt2img—— 用户级基础限额(如 100 次/天) - 次维度:
rate:ip:{$_SERVER['REMOTE_ADDR']}:txt2img—— IP 级兜底防护(如 5 次/分钟) - 强控维度:
rate:api:txt2img:global—— 全局总并发控制(用 Redis 的INCR+EXPIRE配合,防雪崩)
实际调用前,三个键并行检查,任一拒绝即返回 429。注意:uid 必须是登录态可信 ID,不能是前端传的任意字符串;IP 要用 $_SERVER['HTTP_X_FORWARDED_FOR'](若经 Nginx)并做合法性校验,防伪造。
真正难的不是写对 Lua 脚本,而是把“容量”和“速率”的数值配准——配小了影响体验,配大了挡不住攻击。建议先用日志记录真实请求间隔分布,再按 P95 延迟反推速率,容量按业务最大合理并发量设,上线后盯 3 天错误率和桶余量曲线。别信理论值,要看线上水位。



















