文生图接口必须用分布式锁做限流,因其为CPU/GPU密集型任务,单次耗时数秒至十几秒且资源不可复用;普通计数器限流(如incr+expire)在高并发下易因“请求入队但未完成、计数器已重置”导致实际并发超阈值,引发显存溢出或进程卡死;分布式锁通过强制约束“正在执行的任务数”在安全水位线下,与限流器协同实现“准入控制+执行隔离”双保险。

为什么文生图接口必须用分布式锁做限流
因为文生图(如调用 Stable Diffusion API、ComfyUI 后端或自建模型服务)本质是 CPU/GPU 密集型任务,一次请求可能耗时数秒甚至十几秒,且资源不可复用。如果只用普通计数器限流(比如 incr + expire),在高并发下会出现「请求已进队列但未完成,计数器却已重置」的问题——导致实际并发远超阈值,GPU 显存爆掉或 PHP 进程卡死。
分布式锁在这里不是为了互斥访问数据库,而是强制把「正在执行的文生图任务数」卡死在安全水位线下。它和限流器配合,构成「准入控制 + 执行隔离」双保险。
用 Redis SETNX 实现带租约的锁限流
不要直接用 setnx + del 两步,那是经典竞态漏洞。必须用原子写法,并绑定唯一 client ID 和过期时间:
-
SET rate_limit:imggen:{user_id} {client_uuid} EX 30 NX:锁键按用户维度隔离,30 秒是预估单次生成最长耗时(务必比实际 P99 耗时多留 2–3 秒冗余) - 返回
1表示抢锁成功,可继续调用模型;返回0表示当前已有 N 个任务在跑(N = 阈值),直接返回 429 Too Many Requests - 锁释放必须用 Lua 脚本校验
client_uuid,防止 A 拿到锁但超时后 B 覆盖了 key,A 误删 B 的锁
示例 Lua 释放脚本:
立即学习“PHP免费学习笔记(深入)”;
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
锁粒度与阈值怎么配才不翻车
文生图不是 HTTP 短连接,不能按「每分钟请求数」粗放限流。关键看三个硬指标:
- GPU 显存容量:例如 24G V100 最多并行 3–4 个 1024×1024 图像生成任务,那就设上限为
3 - PHP-FPM 子进程数:若
pm.max_children = 20,且每个图像生成占满一个 worker,则锁阈值不能超过 20,否则请求堆积在 FPM 队列里引发超时雪崩 - 用户级 vs 全局锁:对免费用户用
rate_limit:imggen:free:{ip},对付费用户用rate_limit:imggen:pro:{user_id},避免羊毛党挤占资源
错误做法:全站共用一个锁键 rate_limit:imggen:all —— 会导致一个用户卡住,所有人排队。
Redis 连接中断时锁状态怎么兜底
这是生产环境最常被忽略的一环:PHP 进程崩溃、网络闪断、Redis 主从切换,都可能导致锁没释放又没过期。后果是整个文生图服务假死。
- 所有锁键必须带
EX,且时间设为「最大单任务耗时 × 2」,例如实测最长 12 秒,就设EX 25 - 在业务逻辑入口加心跳更新:用
GETSET每 5 秒刷新一次过期时间(仅当当前值匹配自己的client_uuid) - 绝不依赖 PHP 的
register_shutdown_function做锁清理 —— 它在 SIGKILL、OOM killer 场景下完全不触发
真正可靠的兜底,是监控告警:用 redis-cli --scan --pattern "rate_limit:imggen:*" | xargs -I{} redis-cli ttl {} 定期扫出 TTL 异常长的锁键,人工介入或自动熔断该用户请求。



















