想象你是一家生意火爆的餐厅。午餐高峰,客人涌进来,点单五花八门。如果服务员每接一单就冲进厨房大喊“宫保鸡丁一份!”,厨师立刻放下手里的锅去炒新菜,不出十分钟,厨房就会乱成一锅粥——这个菜炒一半,那个菜凉了,抱怨声此起彼伏。 聪明的餐厅会怎么做?服务员把点单小票插到旋转架上,厨师不慌不忙,按顺序一张张取下来做。客人不用催,厨房不会乱。高峰期过了,架上的小票自然会慢慢清空。 这个“旋转小票架”就是任务队列(Task Queue)——计算机世界的“订单槽”。上游业务把耗时任务往队列里一放就返回,下游工人按自己的节奏慢慢处理。它让“产生任务”和“执行任务”彻底解耦,把流量的尖峰削平、低谷填上,这就是削峰填谷。 这篇文章会带你从零理解任务队列的设计原理:队列里有哪些组件、不同模型怎么选、消费者怎么取任务、消息怎么确认不丢失、处理失败怎么办、流量高峰怎么扛,以及上线后怎么盯住它的健康。

岗位关联

任务队列是高并发系统的“承重墙”。无论你做 AI 推理服务、大数据管道还是电商秒杀,理解它都能让你在流量洪峰中保住系统,这是 AI 工程化从业者的核心基本功。

目录
  1. 可视化演示
  2. 核心组成模型
  3. 队列模型分类
  4. 消费者模式
  5. 消息确认机制
  6. 死信与重试
  7. 削峰填谷
  8. 监控与运维
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

核心组成模型

餐厅的旋转小票架,摆在那里自己不会动,让它活起来的是三个角色:插票的服务员、取票的厨师、以及连接两者的架子。任务队列的核心组成模型,恰好也是这三样:生产者(Producer)、队列(Queue) 和 消费者(Consumer)。

生产者就是服务员。业务代码把“需要做的事”封装成一条消息,调用发送接口扔进队列后立刻返回,不再管后续死活。队列是中间的旋转架,它负责暂存消息,是生产者和消费者之间的缓冲带,让两边的节奏互不拖累。消费者是厨师,它从队列里取出一条条消息,真正执行任务逻辑——可能是写一条数据库记录,也可能是一次 AI 模型推理。

这三个角色缺一不可,但真正精妙的是它们之间的解耦:生产者不需要知道消费者是谁、处理得快不快;消费者也不需要关心消息从哪来、之后还会不会有人再来。双方只跟队列打交道,系统因此可以被拆成独立的模块各自演进。

于是出现了工程师最关心的问题:该养多少个消费者才够用?这取决于任务的到达速度和处理速度:

就像餐厅高峰期 100 桌客人、一个厨师同时只能炒 20 桌的菜,那就得安排 5 个厨师同时在灶台前忙碌,旋转架才不会越堆越满。

N=⌈λμ⌉N = \left\lceil \dfrac{\lambda}{\mu} \right\rceil
通俗说:每秒钟来多少任务(λ),除以一个消费者每秒钟能处理多少任务(μ),就是最少需要养几个厨师,除法有余数就向上取整。
  1. 任务到达率 λ单位时间内新进入队列的任务数,由业务流量决定。比如高峰期每秒进来 100 个 AI 生图请求,λ = 100。
    λ=100 任务/秒\lambda = 100 \text{ 任务/秒}
  2. 消费者处理率 μ单位时间内单个消费者能完成的任务数,取决于处理逻辑复杂度和机器性能。假设一个 GPU 服务每秒能处理 20 个请求,μ = 20。
    μ=20 任务/秒⋅消费者\mu = 20 \text{ 任务/秒·消费者}
  3. 计算比值 λ/μ把到达量平摊到每个消费者身上,得到需要的并发消费者数。
    λμ=10020=5\frac{\lambda}{\mu} = \frac{100}{20} = 5
  4. 向上取整消费者数量必须是整数,只要有小数点,就得多雇一个人兜底。
    ⌈λμ⌉=⌈5⌉=5\left\lceil \frac{\lambda}{\mu} \right\rceil = \lceil 5 \rceil = 5

队列模型分类

同样是旋转小票架,不同店的规矩不同。有的店一张点单只让一个厨师做;有的店出了新品,每个窗口都要试做一份;还有的店把川菜和粤菜分开摆、各归各的灶台。队列模型,就是这些规矩。

点对点(Point-to-Point) 是最常见的规矩:一张小票只交给一个厨师,谁手空谁拿,做完就销单。在这类队列里,每条消息只会被一个消费者消费。典型实现如 RabbitMQ 的 Work Queue 和 Redis 的 List 结构,适合“这个任务只需要执行一次”的场景,比如发短信、写日志、跑一次数据清理。

发布订阅(Publish/Subscribe) 则是“广播”规矩:餐厅上了一道招牌菜,每个窗口的厨师都要试做一份。消息会被复制给所有订阅者,每个订阅者都拿到一份独立副本。Kafka 的 Topic、RocketMQ 的广播模式都是这个思路,适合通知、事件总线这类“多系统都要知道”的场景。

分区队列(Partitioned Queue) 解决的是有序性问题:把同一类任务按 Key 扔进同一个分区,由一个固定的消费者处理,保证相同业务的消息严格有序。Kafka 的分区就是这个设计——同一个订单的“下单、支付、发货”事件,必须按顺序被同一个消费者处理,否则可能出现未支付先发货的乱子。

在发布订阅模型里,一条消息会被复制多份:

点对点像“可转小票架,每单一人做”,发布订阅像“新品试做,每个窗口都要来一份”,分区队列则是“川菜归川菜灶、粤菜归粤菜灶”,谁也别抢谁的锅。

T=Np×NsT = N_p \times N_s
通俗说:一条消息最终要存多少份副本,等于生产者数量乘以订阅者数量。广播得越广,存储和带宽成本越高。
  1. 生产者数量 N_p产生原始消息的源头个数。通常发送方只有一路时 N_p = 1。
    Np=1N_p = 1
  2. 订阅者数量 N_s独立订阅该主题的消费者组数量。比如订单组、库存组、通知组都订阅,N_s = 3。
    Ns=3N_s = 3
  3. 副本总数 T每条消息需要复制出的总份数,等于两者乘积。
    T=1×3=3T = 1 \times 3 = 3

消费者模式

厨房里消息怎么传递,直接决定后厨效率。是厨师主动去架子上取(拉),还是服务员端着菜送到灶台(推)?消费者模式,讨论的就是这件事。

推模式(Push):Broker 主动把消息推给消费者。优点是实时性高,消息一进来马上处理;缺点是没有背压(Backpressure)——如果消费者处理不过来,Broker 还在不断推送,消费者会被打爆,只能靠本地缓冲硬扛。WebSocket 长连接推送就是典型的推模式。

拉模式(Pull):消费者主动向 Broker 发起拉取请求,每次拉一批消息回来处理。优点是消费者可以自己控制节奏:处理慢了就少拉,处理快了就多拉,这是天然的背压机制;缺点是实时性稍差,需要轮询或长轮询来弥补。Kafka 就是典型的拉模式。

消费组(Consumer Group) 是生产环境的标配概念:把多个消费者实例编成一个组,组内消息分摊消费,每人处理一部分;组与组之间则像不同的订阅者,各自拿到全量消息。再配合并发控制:每个消费者允许的未确认消息数(prefetch count / max.poll.records)决定了它在本地最多缓存多少条,这是防止消费者内存被打爆的关键旋钮。

在拉模式下,系统的实际输出速率有一个简单的上限:

选推还是拉,本质是选“实时性”还是“主动权”。就像有的后厨喜欢服务员大喊一声提醒,有的后厨偏爱闲下来再去翻小票——各有各的脾气,但大厨房(高并发)里,主动拉取永远更稳妥。

Pout=min⁡(Pin,Cmax)P_{out} = \min(P_{in}, C_{max})
通俗说:队列往外吐消息的速度,不可能超过“上游能给多少”和“下游能吃多少”这两个瓶颈中的较小者。木桶效应在这里同样成立。
  1. 上游可达速率 P_inBroker 单位时间内能提供给消费者的最大消息量,受网络带宽和存储读取速度限制。
    Pin=500 条/秒P_{in} = 500 \text{ 条/秒}
  2. 消费者最大能力 C_max单个或整组消费者单位时间内能处理的最大消息量,受业务逻辑和机器性能限制。
    Cmax=300 条/秒C_{max} = 300 \text{ 条/秒}
  3. 取较小值 min最终有效吞吐取两者中较小者。这个例子里,系统最多每秒处理 300 条。
    min⁡(500,300)=300\min(500, 300) = 300

消息确认机制

如果厨师从架子上拿起一张小票,发现食材不够,又悄悄把小票塞回去走了,那这张单子谁来管?如果厨师做完菜顺手把小票扔了,但账没记上,又算谁的?消息队列面临同样的难题:消费者把消息取走之后,到底算不算处理成功?于是有了消息确认(Acknowledgment)机制。

ACK(肯定确认):消费者处理完一条消息后,主动告诉 Broker“这条我处理好了,可以删了”。Broker 收到 ACK 才会把消息从队列中移除。如果消费者处理到一半进程崩了,没来得及发 ACK,Broker 会在超时后把消息重新交给别的消费者——这就是消息不丢的底气。

NACK(否定确认):消费者明确表示“这条我处理不了”,Broker 可以把它重新入队,或按配置转投死信队列。超时重投:消费者拿到消息后如果一直不回应,Broker 会认为它“死亡”了,把消息重新投递给其他消费者。

由此引出了消息投递语义。最常见的“至少一次投递”意味着:消息可能会被重复处理。实际处理次数与“最少一次”的关系是:

ACK 就是厨师打完勾再扔小票,NACK 是遇到做不了的菜主动举手下单重排,超时则是“厨师晕倒了,店长把他的单子重新分给其他人”。确认机制的存在,让后厨永远知道每张单子的最终下落。

A≥1A \ge 1
通俗说:在至少一次语义下,每条消息最少处理一次,但因为超时重投和 NACK 重试,实际处理次数可能多于一次。所以消费者的处理逻辑必须“幂等”——处理两次和一次结果相同,否则就会重复扣款、重复发短信。
  1. 最少投递次数 1Broker 保证每条消息一定会被投递并被处理到至少一次,不会凭空消失。
    Dmin=1D_{min} = 1
  2. 实际处理次数 A由于可能发生重试、重投,A 可能大于 1。
    A=1,2,3,…A = 1, 2, 3, \ldots
  3. 幂等等价应用层要把多次执行压缩成一次的效果,例如用唯一订单号去重。
    Effect(A)≡Effect(1)\text{Effect}(A) \equiv \text{Effect}(1)

死信与重试

一张小票被重做了三次还是失败,比如客人点了“清蒸鲥鱼”,但今天市场根本没这食材。厨师不可能无限重试下去,那样只会让后厨越积越多。现实中的方案是:设置最大重试次数,超过之后把做不了的菜放进专门的筐里——这就是死信队列(Dead Letter Queue, DLQ)。

重试策略:简单的重试是“失败了马上再来一次”,但这样会带来可怕的问题——如果下游数据库挂了,所有任务同时重试,等于把数据库再压垮一次,这叫重试风暴。于是有了指数退避(Exponential Backoff):每次重试的间隔时间呈指数增长,先 1 秒、再 2 秒、再 4 秒……并且加入随机抖动(Jitter),避免所有任务在同一时刻集体重试。

死信不是垃圾桶,而是事故的“黑匣子”。工程师要监控死信队列的入队率,一旦异常迅速排查,必要时人工补处理。就像餐厅里“做不了的菜”,店长要定期翻废单筐,看看是食材供应链的问题,还是菜单设计的问题——这正是死信队列在 AI 推理里用来排查“永远失败的坏请求”的原因。能救的菜反复救,救不了的进废单筐留档,既不浪费食材,也不让一张坏单拖垮整个后厨。

Waiti=Base×2i−1+jitterWait_i = Base \times 2^{i-1} + \text{jitter}
通俗说:第一次重试等 1 秒,第二次等 2 秒,第三次等 4 秒……每多失败一次,就给系统多一点恢复的时间。那一小撮随机抖动,是为了防止“大家一起重来”的惊群效应。
  1. 基础等待 Base第一次重试前等待的初始时间,比如 1 秒。
    Base=1 秒Base = 1 \text{ 秒}
  2. 指数增长部分 2^(i-1)第 i 次重试的间隔系数,i=1 时是 1,i=2 时是 2,i=3 时是 4。
    2i−1=1,2,4,8,…2^{i-1} = 1, 2, 4, 8, \ldots
  3. 随机抖动 jitter在固定间隔上叠加一个随机偏移,比如 ±30%,防止集体重试。
    jitter∈[−δ,+δ]\text{jitter} \in [-\delta, +\delta]

削峰填谷

午餐高峰 200 桌客人的菜,不可能同时下锅。餐厅的办法是让客人拿号排队,厨房按节奏一桌桌做——把涌进来的尖峰,削成均匀的平坡。这就是削峰填谷。

削峰填谷是任务队列最出圈的价值,也是秒杀系统和 AI 推理服务必备的能力。假设请求到达率瞬间爆发到每秒 5000 个,而下游处理能力只有每秒 1000 个,没有队列时这 5000 个请求会直接把下游打挂;有了队列,请求先进队列“拿号排队”,消费者依然以每秒 1000 个的稳定节奏消化。流量曲线从“尖峰”变成“平坡”,这就是削峰。低谷期队列里没活可干,系统慢慢把积压清空,这就是填谷。

流量缓冲是队列的天然属性:生产者可以以任意速度往队列里写,消费者按自己的节奏读。批量消费是进一步提效的手段:把短时间内的消息攒成一批再统一处理,比如 Kafka 的批量拉取、数据库的批量写入,能显著摊薄网络开销和 IO 次数。限流保护则给这口“大锅”装上安全阀:积压超过阈值时拒绝新任务或触发熔断,防止无限积压拖垮 Broker 本身。

在一次突发流量中,积压量可以这样快速估算:

削峰填谷的本质,是拿“时间”换“稳定”。客人多等几分钟没关系,但厨房一旦炸锅,所有人都吃不上饭。队列正是用这种“先排号、再上菜”的策略,帮系统扛过流量高峰。

ΔQ=(λpeak−μ)×T\Delta Q = (\lambda_{peak} - \mu) \times T
通俗说:每秒多进来的任务数,乘以突发持续的时间,就是会堆在队列里的积压任务数。这个数字大到超过队列容量时,就必须限流或扩容消费端。
  1. 峰值到达率 λ_peak突发期间每秒涌入的任务数,比如促销 30 秒内每秒 5000 个请求。
    λpeak=5000 个/秒\lambda_{peak} = 5000 \text{ 个/秒}
  2. 系统处理速率 μ消费者集群每秒能处理的任务数,比如 1000 个/秒。
    μ=1000 个/秒\mu = 1000 \text{ 个/秒}
  3. 差值 (λ_peak - μ)每秒“消化不掉”的任务数,即积压增长速度。
    5000−1000=4000 个/秒5000 - 1000 = 4000 \text{ 个/秒}
  4. 乘以时间 T若突发持续 30 秒,总积压量就是 120,000 个任务。
    4000×30=120,000 个4000 \times 30 = 120{,}000 \text{ 个}

监控与运维

店长怎么知道前厅和后厨快不行了?看三点:架子上压了多少小票(积压),客人从下单到上菜要多久(延迟),服务员和厨师是否还忙得过来(健康)。任务队列上线之后,同样需要这三块“仪表盘”。

积压量(Backlog):队列里当前未处理的消息总数,是系统最直观的健康指标。积压平稳说明生产消费平衡,积压持续攀升说明消费者跟不上,很快就要出大事。消费延迟(Lag/Delay):最老的那条消息在队列里等了多久。它比积压量更贴近用户体验——即使积压量不大,如果新消息一直插队、老消息没人管,延迟也会暴露问题。健康检查(Health Check):对 Broker 节点、消费者实例做存活探活,配合消费进度监控,判断消费者组是否还活着、有没有卡死。

有了指标,还要有告警。核心规则是:积压量超过阈值并持续一定时间,或消费延迟超过业务容忍度,立刻触发告警,把值班工程师从睡梦中叫起来。

估算延迟的核心公式:

店长不只需要好厨师,还需要一块能随时看到“小票架剩余量”的仪表盘。任务队列的监控,就是给整个异步系统装上仪表盘——积压、延迟、健康,三块表一目了然,系统才能在风暴来临前提前预警,而不是等崩溃了才拍大腿。

Delay=QbacklogμconsumeDelay = \frac{Q_{backlog}}{\mu_{consume}}
通俗说:把当前积压的消息数量,除以每秒钟能消费的速度,得到的秒数就是“清空队尾需要多久”。这个数字超过 10 分钟,就该让告警响了。
  1. 积压消息数 Q_backlog队列中当前未被消费的消息总量,比如 10,000 条。
    Qbacklog=10,000Q_{backlog} = 10{,}000
  2. 消费速率 μ_consume消费者集群单位时间能成功消费的消息数,比如 500 条/秒。
    μconsume=500 条/秒\mu_{consume} = 500 \text{ 条/秒}
  3. 相除得到延迟秒数10,000 ÷ 500 = 20,意味着把现有存量全部处理完需要 20 秒。
    10,000500=20 秒\frac{10{,}000}{500} = 20 \text{ 秒}

AI 应用场景

大模型推理服务排队

阶段:模型推理与服务部署

解决的问题:文生图、大语言模型 API 的请求量波动极大,凌晨无人问津、上班时间瞬间打满。GPU 实例昂贵,按峰值扩容浪费资源,按均值扩容又会被打崩。通过把请求放入任务队列,GPU 后端以恒定速率消费,高峰期排队、低谷期清空积压,既保证服务稳定又提高 GPU 利用率。

具体案例:某 AIGC 公司的文生图服务接入队列后,峰值延迟延后约 15 秒,但彻底消除了 5xx 错误,GPU 利用率从 40% 提升到 85%。

数据标注任务的异步管道

阶段:数据工程

解决的问题:标注平台中,上传原始数据、触发预标注模型、分派给标注员、通知入库,这些环节相互依赖又节奏不同。传统同步调用会导致上传接口被预标注任务拖慢。使用任务队列后,各环节只依赖队列,互不阻塞,上游上传完毕立即返回。

具体案例:数据管道上传 10 万张图片后立即返回成功,后台任务队列依次完成 AI 预标注、人工审核、格式转换和入库,全程进度可查询。

模型训练任务的失败重试

阶段:模型训练

解决的问题:训练任务常因 GPU 抢占、数据源抖动、网络超时而失败。直接失败浪费算力,无限重试又可能打崩数据服务。用任务队列配合指数退避和死信队列:训练任务失败后自动重试 3 次,仍失败进入死信队列,供调度平台人工排查。

具体案例:某公司 Lora 微调任务日均 2000 个,接入队列后失败自动重试,死信率稳定在 0.3%,全部失败任务可追溯、可重放。

Python 演示

python67 行
import queueimport randomimport threadingimport time  class MiniTaskQueue:    """迷你任务队列: 支持 ACK 确认 / 失败重试 / 死信回收"""     def __init__(self, worker_num=2, max_retry=3):        self.q = queue.Queue()        self.dlq = queue.Queue()        self.retry_count = {}        self.max_retry = max_retry        self.lock = threading.Lock()        self.workers = [            threading.Thread(target=self._work, daemon=True)            for _ in range(worker_num)        ]     def produce(self, task):        self.q.put(task)        print("[入队]", task)     def _ack(self, task):        print("[成功]", task)     def _nack(self, task):        with self.lock:            self.retry_count[task] = self.retry_count.get(task, 0) + 1            if self.retry_count[task] >= self.max_retry:                self.dlq.put(task)                print("[死信]", task)            else:                self.q.put(task)                print("[重试]", task, "第", self.retry_count[task], "次")     def _work(self):        while True:            task = self.q.get()            if random.random() < 0.3:                self._nack(task)            else:                self._ack(task)            self.q.task_done()            time.sleep(0.2)     def start(self):        for w in self.workers:            w.start()     def join(self):        self.q.join()        print("剩余任务:", self.q.qsize(), "死信任务:", self.dlq.qsize())  def main():    mq = MiniTaskQueue(worker_num=2, max_retry=3)    mq.start()    for i in range(10):        mq.produce("task-" + str(i))        time.sleep(0.1)    mq.join()  if __name__ == "__main__":    main()
目的:一个约 60 行的迷你任务队列,演示生产者投递、多消费者并发处理、ACK 确认、失败重试和死信回收五个核心机制。
  1. self.q = queue.Queue()队列存储层,生产者和消费者之间唯一的“接驳点”,天然线程安全
  2. def produce(self, task):生产者接口:把任务塞进队列立刻返回,不等待处理结果
  3. def _ack(self, task):确认机制:任务处理成功后打印并移除(这里用出队代替)
  4. def _nack(self, task):拒绝机制:登记重试次数,超限进死信队列,否则重新入队
  5. if random.random() < 0.3:模拟 30% 的失败率,制造重试与死信场景
  6. self.q.task_done()通知队列本任务消费完毕,join() 会据此判断全部任务是否结束

常见误区

误区把任务放进队列之后,接口确实秒回了,队列是不是把任务本身也加速了?那以后所有慢任务都塞队列好了。

正解队列没有加速任何任务,它只是把“提交”和“执行”拆开,让调用方不用傻等。任务本身的执行时间一点没变,反而还多了一段排队延迟。你感受到的“快”是接口响应变快了,整体吞吐变大了,但单个任务的处理时间并没有缩短。

为何常见:接口从 5 秒变成 50 毫秒,直觉上就是“变快了”,容易把感知延迟和计算耗时混为一谈。

误区我业务逻辑都执行完了,消息自然就没了,不用管什么确认吧?RabbitMQ 不是自动删除吗?

正解几乎所有主流队列都把“确认”当成契约。消费者必须显式 ACK,Broker 才会认为消息已处理完成;不 ACK,Broker 会在超时后重新投递,导致同一任务被处理多次,造成重复扣款、重复发短信等事故。自动确认是开发调试时的简化开关,生产环境必须手动确认。

为何常见:很多人把消息队列想象成 HTTP 调用,认为“收到即成功”,忽略了异步系统中“确认”和“投递”是两件独立的事。

误区队列积压说明消费者不够,那就水平扩容,加到积压消失为止。

正解积压的瓶颈往往不在消费者本身,而在下游共享资源——数据库连接池、第三方 API 限流、AI 模型的 GPU 并发。盲目加消费者只会让下游更拥挤,积压不减反增。扩容前先看消费者日志和下游耗时。如果瓶颈在数据库,扩容消费者反而是在给数据库火上浇油。

为何常见:“水平扩展”是最响亮的扩容口号,但很多人忽略了下游资源“不可无限扩展”的现实约束。

误区死信队列里都是处理失败的消息,是垃圾数据,定期清空释放空间就行了。

正解死信是系统里“反复失败”的任务集合,几乎总是意味着代码 bug、数据异常或外部依赖故障。正确做法是监控死信入队速率、配置实时告警,并对死信做人工补偿或分析,而不是删掉。一条死信就是一次尚未爆炸的事故,它是排查问题的黄金线索。

为何常见:看到“Dead“字样就觉得是垃圾,忽视了它作为”事故黑匣子”的诊断价值。

面试题

Q1. 任务队列最核心的价值是什么?

单选题 · easy

  • A.提升单次任务处理速度
  • B.解耦系统并削峰填谷← 正确答案
  • C.减少代码编写的工作量
  • D.提高数据库的读写容量

答案:B。任务队列本质是中间缓冲,不加速单任务,也不减少代码量或提升数据库容量。它让生产者和消费者的节奏解耦,把流量尖峰削平,这才是高并发系统的核心价值。

Q2. 消费者成功处理完一条消息后,最应该做什么?

单选题 · easy

  • A.立即发送 ACK 确认← 正确答案
  • B.保持沉默不做操作
  • C.重新投递给其他消费者
  • D.删除该消息对应数据

答案:A。ACK 是为了告诉 Broker“这条消息处理完成,可以移除”;不 ACK 会导致超时重投、重复处理。删除数据是业务逻辑而非队列语义,重新投递方向恰好反了。

Q3. 点对点队列与发布订阅队列的核心区别是什么?

单选题 · mid

  • A.点对点每条消息仅一个消费者消费← 正确答案
  • B.发布订阅不需要生产者来发送消息
  • C.点对点不支持大规模高并发场景
  • D.发布订阅的消息不会丢失也不重复

答案:A。点对点是“竞争消费”,一条消息只服务一个消费者;发布订阅是“广播”,每个订阅者都收到一份。B、C、D 都不符合两种模型的实际语义。

Q4. 消息积压持续增长,最直接的原因是什么?

单选题 · easy

  • A.生产速度大于消费速度← 正确答案
  • B.网络带宽偶尔出现波动
  • C.消费者数量配置过多
  • D.队列存储空间不太足

答案:A。积压量 = 进入速度 - 离开速度。生产持续快于消费,队列才会越堆越长。消费者过多反而消化得更快,网络波动影响的是瞬时而非持续,存储空间不足是积压的结果而非原因。

Q5. 指数退避重试中,若基础等待时间 1 秒,第 3 次重试前大约等待多久?

单选题 · mid

  • A.约 1 秒
  • B.约 2 秒
  • C.约 4 秒← 正确答案
  • D.约 8 秒

答案:C。第 i 次重试等待 base × 2^(i-1),i=3 时是 1×2²=4 秒。A、B 对应前两次等待,D 对应第 4 次等待。指数退避的意义是给下游越来越长的恢复时间。

Q6. 相比推模式,拉模式消费方式的主要优势是什么?

单选题 · mid

  • A.消息投递实时性更高
  • B.消费者可自主控制消费节奏← 正确答案
  • C.消费者无需确认处理结果
  • D.消息绝对不会发生重复

答案:B。拉模式下消费者按自身能力主动拉取,天然具备背压保护。A 其实是推模式的优势;C 和 D 都与模式无关,确认和去重是所有模式的通用问题。

Q7. 以下哪一项不属于典型任务队列的核心组件?

单选题 · hard

  • A.生产任务消息的调用方
  • B.承担缓冲存储的中间层
  • C.拉取并处理消息的执行方
  • D.负责编译部署任务的编排器← 正确答案

答案:D。核心组件是生产者(A)、队列(B)、消费者(C)。负责编译部署的是 CI/CD 流水线或调度系统,不属于任务队列模型内的组件,它是队列的“外部环境”。

Q8. 消息投递语义包含以下哪些类型?

多选题 · mid

  • A.至多一次投递← 正确答案
  • B.至少一次投递← 正确答案
  • C.恰好一次投递← 正确答案
  • D.无限次投递

答案:ABC。标准语义有三种:at-most-once(至多一次,可能丢失)、at-least-once(至少一次,可能重复)、exactly-once(恰好一次,理想语义)。无限次投递不是标准语义,那是重试失控时的故障表现。

Q9. 系统出现消息积压后,合理的处置手段有哪些?

多选题 · hard

  • A.增加消费者的实例数量← 正确答案
  • B.开启消费端的批量处理← 正确答案
  • C.将队列中的消息全部丢弃
  • D.提高消费任务的并发度← 正确答案

答案:ABD。增加实例、批量处理、提高并发都能提升消费速率。丢弃消息会造成数据丢失,除非业务明确可容忍,否则绝不应作为常规手段。

Q10. 死信队列在工程实践中可以发挥哪些作用?

多选题 · hard

  • A.定位反复失败的异常任务← 正确答案
  • B.人工介入补偿处理坏数据← 正确答案
  • C.延迟重放修复临时性问题← 正确答案
  • D.替代主队列承担全部流量

答案:ABC。死信是“反复失败”任务的聚集地,可用于定位异常、人工补偿、条件重放。用死信替代主队列会违背设计初衷,导致所有消息都走异常通道,完全不可取。