← 服务端与数据 · 第 3 篇

请求太多时:任务队列、缓存与背压

用有界资料任务队列连接接收、确认、重试与去重,分清限流背压和缓存陈旧策略的职责。

学完这一篇,你能做到

定义任务容量与恢复规则,并验证满载、重复、永久失败和缓存缺失不会造成错误效果。

资料预览任务需要较长处理,许多请求一起到达时,把每个任务都留在 HTTP 连接里会占满资源。队列可以把接受与完成分开,缓存可以复用结果,但两者不会自动让容量无限,也不会保证任务只执行一次。

案例是合成的资料转换队列,容量两条,重复任务只产生一个效果。代码在进程内使用 deque,不连接消息代理、Redis 或真实工作进程。目标是定义满载、重试与结果状态,再认识这个模型缺少的持久化保证。

接受任务不等于处理完成

异步接口可以先返回任务标识,客户端再查询进度。接受时需要确认输入、权限、容量和任务记录是否可靠保存。只有排进某个内存列表,进程重启后就可能丢失,不能向用户承诺任务一定完成。

状态至少区分等待、执行、成功与失败。取消应有明确语义:等待任务可以移除,执行中的任务是否可停止取决于处理方式。结果可下载也需要独立检查权限、有效期和保存位置,不能凭知道任务 ID 就读取任何素材。

队列容量与消费者并发共同限定在途工作。队列无限增长会把延迟转成内存、磁盘或等待时间问题。决定超额请求是拒绝、延迟还是进入有预算的等待,并让用户看见实际状态,不要永远展示正在处理。

限流、背压和并发上限各自作用

限流控制一段时间允许多少请求,超过时明确拒绝或按规则排队。背压让下游容量不足时减慢上游生产。并发上限控制同时执行的任务数;队列容量控制等待量。它们可配合,但不能用一个名称概括所有负载策略。

按用户或租户分配预算可以避免一个请求来源占满资源,同时还要有全局上限。估计任务成本时考虑文件体积、处理阶段与外部依赖,不能只计任务条数。取消、超时和完成后及时释放容量。

客户端也要有总等待与重试预算。服务返回忙碌时立即无限重试,会把拒绝转成更高负载。观察排队时间、处理时间、拒绝量和失败类型,再决定增加消费者还是限制输入;队列很长不直接证明需要更多线程。

确认与去重保护不同边界

RabbitMQ 的 publisher confirms 关注发布方与代理之间的接受确认,consumer acknowledgements 关注消费者已处理交付。它们不能互相替代。自动确认可能在实际处理前让代理认为消息已交付,消费者崩溃时就失去重投机会。

手动确认通常在所需结果可靠提交后进行,但提交后、确认前崩溃仍可能导致重复交付。业务效果需要持久化去重、唯一约束或事务设计,不能从“启用了 ack”推导恰好一次。

任务 ID 去重还要验证输入一致。同一 ID 被用于不同内容时应按契约拒绝或返回冲突,不直接跳过。去重记录与实际效果必须协调;先记已完成再执行效果会丢工作,先执行后独立记完成会留下重复窗口。

重试与毒消息需要终点

临时依赖失败可以在次数和时间预算内退避重试,并用抖动减少同时重试。格式不合法、权限不足或不支持的输入通常不是重试能修复的失败,应直接进入明确终态。

重复失败的毒消息需要隔离、记录安全原因和人工检查路径,避免立即重新入队形成死循环。死信或隔离机制也需要容量与告警策略,不能把失败永久藏进另一个队列就称为恢复。

检查任务超时是否意味着处理真的停止。若外部调用继续执行,重试可能同时创建第二份结果。通过结果关联、状态查询和补偿规则处理不确定完成,别在缺少证据时一律重新发起有副作用动作。

离线观察有界队列与重复效果

队列最多容纳两条。重复 J-1 可以占两个队列位置,消费时 processed 集合让 effects 只追加一次;第三条在满载时拒绝。这个设计展示容量与效果去重独立,未在接受阶段合并重复任务。

from collections import deque
queue = deque()
processed = set()
def accept(job):
    if len(queue) >= 2:
        return False
    queue.append(job)
    return True
assert accept("J-1") and accept("J-1")
assert not accept("J-2")
effects = []
while queue:
    job = queue.popleft()
    if job not in processed:
        effects.append(job)
        processed.add(job)
assert effects == ["J-1"]
print(effects)

输出为 ['J-1']。重复交付只生成一个合成效果,但内存集合与队列都会在重启后丢失,也没有多消费者原子检查。它不能作为真实任务系统的恰好一次实现,只验证顺序执行下的规则。

如果接受阶段就查询 processed,仍要考虑任务尚未完成的重复和并发检查。真实系统可以为请求建立唯一记录,事务中协调状态和副作用;具体设计取决于效果是否与记录在同一数据库。

缓存必须说明可接受的旧数据

缓存键包含资源、版本和必要的租户范围,避免不同用户共享私有结果。TTL 限制保留时间,但不能保证每次读取都是最新值。写后失效、版本键或明确的陈旧窗口,需要按数据用途选择。

Redis 的容量与淘汰策略管理内存;缓存可能在 TTL 前被淘汰,应用必须能正确回源。失效时大量请求同时重算形成惊群,可用合并请求或有限刷新等策略减轻,但别把旧权限判断长期缓存而不考虑撤销。

缓存不应成为唯一任务事实来源。结果缓存丢失可以重新获取,任务记录与所有权不能随淘汰消失。对不可重复效果,回源应查询原结果而非再次执行任务。

队列等待还要区分公平性。一个大任务长期占用消费者时,短任务可能同样超时;可以按成本或类别设置独立预算,但需要防止某类任务永久饥饿。

练习:验证满载与恢复

定义容量、并发、每租户预算、任务状态、重试条件与去重结果。用重复消息、超额请求、临时失败、永久失败和处理后确认前崩溃构造教学时间线,说明每一步如何恢复。

验收要求:满载可见且不无限增长;重复成功不重复效果;永久失败有终态;重试耗尽进入隔离;缓存缺失能恢复结果而不再执行副作用。真实代理的重投与重启需要独立测试,deque 断言不覆盖持久化和多消费者并发。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。