关于泄漏锁的论证:将 Redis TTL 用作昂贵 AI 任务的失败冷却
Source: Dev.to
我没预料到的问题:释放锁让我花钱
我的任务队列很简单:用户提交文档 → AI 进行评估 → 将结果存储。
AI 调用可能会失败——虽然很少见,但确实会发生。模型可能会忽略预期的输出格式,或者触发速率限制。我捕获异常,记录日志,将任务标记为失败,并在 finally 块中释放锁。
当用户重试时,AI 再次失败,导致一次重试风暴,冲击 LLM API 并产生实际费用。
使用锁过期作为冷却机制
# Acquire a lock with a 5‑minute TTL
lock_key = f"ai_job_lock:{job_id}"
acquired = redis.set(lock_key, "1", nx=True, ex=300) # nx=True → set only if not exists
# If the lock already exists, the job is either running or cooling down
if not acquired:
return
try:
result = await call_llm_api(data)
save_result(result)
# Release lock only on success
redis.delete(lock_key)
except Exception as e:
log.error(f"Failed: {e}")
# Do NOT release the lock; the TTL provides the cooldown window锁会保持五分钟,随后 Redis 自动驱逐它。这不是内存泄漏——它是一个自毁的键。
Without TTL: fail → retry → retry → retry → 20 calls in 60 s
With TTL: fail → blocked → blocked → retry at t=5 min为什么可行
- 速率限制 – 立即重试会再次触发同一限制。
- 网络抖动 – 通常在几分钟内恢复,而不是瞬间。
- 提示问题 – 无论重试频率如何都不会自行解决。
当系统已经承受压力时,再增加昂贵的 LLM 调用只会让情况更糟。基于 TTL 的冷却为外部服务提供恢复时间,同时保持用户可见的延迟较低。
我真正喜欢的部分
没有复杂的重试逻辑、指数退避或状态机——只有时间。
# All the code you need
acquired = redis.set(lock_key, "1", nx=True, ex=300)
# On failure, just let it ride; the lock expires naturally如果工作进程在任务中途崩溃,锁仍会过期,允许恢复服务稍后接手任务。在冷却窗口期间,任何新的重试请求只会因为获取不到锁而提前退出,且不会调用 LLM。
不适用的情况
- 费用低廉的操作,重试几乎是免费的。
- 必须立即重试的任务。
- 用户期望同步、即时响应的场景。
值得提及的风险
锁时长不匹配
如果任务运行时间超过 TTL,锁可能在任务仍在执行时过期,导致另一个工作进程接手。确保 TTL 超过最坏情况运行时间,或实现一个心跳来刷新锁。
确定性失败
冷却对瞬时问题(速率限制、网络故障)有帮助,但对永久性问题(错误输入或提示损坏)无效。应将此类失败标记为永久失败,而不是让它们每五分钟循环一次。
面向用户的反馈
如果用户重试后看不到进展,会觉得系统出错。提供诸如 “X 分钟后可重试” 或任务状态指示,让用户知道系统正在等待,而不是静默卡住。
总结
我们常常花大量精力构建复杂的重试机制、断路器和回退系统。有时最简单的答案就是:让它失败,等一会儿,再稍后重试。Redis 的 TTL 自动为你提供了“等一会儿”,几乎不需要额外代码,也不会出现意外绕过的风险。
想知道有没有人也尝试过这种方式或有更好的方案——欢迎分享!