关于泄漏锁的论证:将 Redis TTL 用作昂贵 AI 任务的失败冷却

发布: (2026年3月14日 GMT+8 00:25)
5 分钟阅读
原文: Dev.to

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 自动为你提供了“等一会儿”,几乎不需要额外代码,也不会出现意外绕过的风险。

想知道有没有人也尝试过这种方式或有更好的方案——欢迎分享!

0 浏览
Back to Blog

相关文章

阅读更多 »