文生图接口限流需控并发任务数而非请求频次,应采用加权令牌桶(按分辨率/Refiner权重扣令牌)和Redis队列状态检查;重试须区分503/504排队(轮询结果)、202已入队(禁止重发)、4xx错误(立即终止)。

PHP 文生图接口(如调用 Stable Diffusion API、DALL·E 封装服务或本地 ComfyUI 接口)必须限流——这类请求 CPU/GPU 负载高、耗时长(常 3–30 秒),不加限制极易被刷爆队列或触发 OOM。重试机制也不能照搬普通 HTTP 请求那一套,否则会雪上加霜。
为什么固定窗口计数器在文生图场景下基本失效
固定窗口(如 INCR + EXPIRE)看似简单,但对文生图接口有致命缺陷:
- 窗口内允许 10 次请求,但若用户连续发 10 个高分辨率图任务,后端可能积压 200 秒处理时间,实际并发远超预期
-
EXPIRE到期只清计数,不清正在执行的任务,Redis 计数归零 ≠ 后端压力归零 - 无法区分“已排队”和“已完成”,容易造成虚假放行
真正要控的是**同时运行中的任务数**,不是“过去 60 秒发了多少次请求”。建议改用基于 Redis List 的轻量队列状态检查,或直接对接任务队列(如 Redis Queue / Beanstalkd)的 llen 或 stats-tube。
令牌桶算法需改造:按“任务权重”而非“请求数”扣令牌
文生图请求差异极大:一张 512×512 图和一张 1024×1024+Refiner 图消耗资源可能差 5 倍。原生令牌桶按次扣 1 个令牌会失衡。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 定义任务权重:例如
base_weight = 1(512×512),hd_weight = 3(1024×1024),refiner_weight = 2 - 令牌桶键名用
rate_limit:task:{user_id},值结构为 JSON:{"tokens": 10, "last_fill": 1724720160, "capacity": 20} - 每次请求前,先解析参数算出
$weight,再用 Lua 脚本原子化判断并扣除:if tokens >= weight then tokens = tokens - weight - 填充逻辑仍按固定速率(如每秒 +0.5 token),但最大补到
capacity,避免突发囤积
这样既能防爆,又允许小任务穿插执行,比纯计数更贴合真实负载。
重试不能无脑 sleep,要识别“真失败”和“假排队”
文生图接口返回 503/504 不等于失败——很可能是后端队列满、GPU 忙,稍等即可;但返回 400(参数错)、401(key 过期)重试毫无意义。
关键判断点:
- HTTP 状态码是
503或504,且响应体含"queue_position"或"estimated_wait"字段 → 属于排队中,应等待而非重试 - 收到
202 Accepted+Location头 → 已入队,后续轮询结果接口,**禁止重发原始请求** - curl 错误码为
CURLE_OPERATION_TIMEOUTED且耗时 > 30 秒 → 很可能卡死,可重试,但最多 1 次 - 任何
4xx错误(除429外)立即终止,记录日志,不重试
示例轮询逻辑片段:
$jobId = $response['job_id'];
for ($i = 0; $i < 12; $i++) {
$res = $this->get("/result/$jobId");
if ($res['status'] === 'succeeded') {
return $res['image_url'];
}
if ($res['status'] === 'failed') {
throw new Exception($res['error']);
}
usleep(2000000); // 等 2 秒再查,不盲目重发 POST
}
文生图限流的核心不在“拦多少请求”,而在“看清每个请求的真实代价”;重试的关键也不是“多试几次”,而是“分清哪些该等、哪些该停、哪些根本不用动”。漏掉这点,限流就只是纸面数字,重试反而成压垮服务的最后一根稻草。



















