想象你在网上订购了一台咖啡机。卖家把机器打包,交到快递站,包裹被运往城市中转仓,最后由快递员送到你家。这一路有三个角色:寄件人、中转仓库、收件人。如果现在你问:“怎么保证包裹一定到我家、不会丢、更不会让我为同一台机器付两次钱?”——这,正是消息系统里“消息可靠投递”要回答的问题。 在 AI 工程化的世界里,服务之间很少“当面打电话”,而是把消息装进包裹,交给一个叫“消息队列(MQ)”的中转站。大数据管道、微服务通知、日志采集,全靠它传递。但网络会闪断、服务器会宕机、进程会崩溃——一个包裹丢了,AI 系统就可能基于缺失的数据做出错误决策。好消息是,工程师们已经总结出一整套“快递级”的可靠运输方案:寄件要回执、仓库要有账本、派件要签收、问题件要进仓库、全程记录要可追溯。本文就借这趟快递之旅,把“不丢不重”的工程机密讲透。

岗位关联

消息可靠投递是 AI 数据管道和微服务架构的“地基工程”。掌握它,你就能独立设计高可靠的数据链路,在模型训练、实时推理、业务通知等场景中确保“消息不丢、业务不重”,这是 AI 工程化中最值钱的硬技能之一。

目录
  1. 可视化演示
  2. 可靠性挑战
  3. 生产者确认
  4. 消费者幂等
  5. 消息持久化
  6. 死信策略
  7. 消息回溯
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

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

deliver failed消息可靠投递流程:生产者发送消息经 Broker 持久化后投递到消费者,acks 级别决定确认强度,失败消息进入重试或死信队列。

可靠性挑战

快递会丢在哪儿?可能是快递员取件时记错了地址,可能是中转仓遭遇火灾,也可能是派件时收件人刚好搬家。任何一个环节掉链子,包裹就到不了你手上。消息系统同样如此,三个环节缺一不可:生产者(Producer)负责发消息,消息代理(Broker)负责暂存和转发,消费者(Consumer)负责最终处理。可靠性工程的任务,就是让这三个环节的意外尽量不造成消息的永久丢失。

按照丢失的严重程度,行业定义了三种投递语义。“最多一次”(At-Most-Once)很洒脱:丢了就丢了,适合日志采样这种允许缺失的场景;“至少一次”(At-Least-Once)很执着:保证不丢,但可能重复;“精确一次”(Exactly-Once)最完美,成本也最高。工程实践上,大家普遍用“至少一次 + 消费者幂等”的组合拳,以很小的代价获得近似精确一次的效果。

为什么要把三个环节放在一起看?因为可靠性是乘法关系,不是加法。

就像一台咖啡机的运输链上只要有一个仓库不靠谱,你收到咖啡机的概率就会断崖式下降。所以,接下来我们从寄件环节开始,把每一环都管住。

R总=R生产⋅R中转⋅R消费\displaystyle R_{\text{总}} = R_{\text{生产}} \cdot R_{\text{中转}} \cdot R_{\text{消费}}
整条链路的可靠率等于三个环节可靠率的乘积,任何一个环节不是 100% 可靠,整体就会被拉垮。
  1. 可靠率符号R 表示某个环节“不丢消息”的概率,取值在 0 到 1 之间,1 代表绝对可靠。
    R∈[0, 1]R \in [0,\,1]
  2. 乘法连接包裹要成功送达,寄件、中转、派件三个环节必须同时成功,所以端到端可靠率是三者相乘。
    R总=R生产×R中转×R消费R_{\text{总}} = R_{\text{生产}} \times R_{\text{中转}} \times R_{\text{消费}}
  3. 木桶效应因为每个 R 都小于等于 1,乘积只会比最小的一项还小,最弱的环节决定了系统的天花板。
    R总≤min⁡(R生产, R中转, R消费)R_{\text{总}} \le \min(R_{\text{生产}},\,R_{\text{中转}},\,R_{\text{消费}})

生产者确认

寄快递时,快递员会给你一张回执单,证明包裹已被揽收。消息系统里也有类似回执——生产者发送消息后,Broker 接收到消息会返回一个 ACK(确认回执)。如果发送后迟迟等不到 ACK,生产者该怀疑包裹在路上丢了,于是重新发送。

工程上这个机制有两种玩法。同步发送:发消息后阻塞等待 ACK,适合订单、支付这类关键业务,慢但踏实。异步回调:发完继续干别的,Broker 确认后通过回调通知,吞吐量高,适合日志、埋点这类大规模场景。无论哪种方式,重试都不是无限重发——要有上限,还要配合“退避”策略,比如失败后先等 200 毫秒再重试,下次翻倍,避免把网络打成“拥堵路段”。更进阶的是事务消息:生产者先问 Broker“能收吗”,Broker 暂存半消息,等本地事务提交成功后才真正把消息对外可见,保证“要么都成功、要么都失败”。

重试的价值可以用一个概率公式看清:

有了回执单,消息从“我发出了”变成“中转站确实收到了”。但请注意:重试也埋下了一个隐患——同一份包裹可能被重复投递,这就轮到下一节“收件登记本”登场了。

P成功=1−(1−p)n\displaystyle P_{\text{成功}} = 1 - (1-p)^{n}
单次投递成功率只有 p,重试 n 次后,至少成功一次的概率就是 1 减去“每次都失败”的概率。
  1. 单次失败率假设单次发送成功的概率是 p,那么失败的概率就是 1-p,记作 q。
    q=1−pq = 1 - p
  2. n 次全部失败如果每次发送相互独立,重试 n 次全都失败的概率是 q 自乘 n 次。
    qn=(1−p)nq^{n} = (1-p)^{n}
  3. 至少一次成功用整体概率 1 减去“全部失败”的坏情况,就得到最终成功率。
    P成功=1−(1−p)nP_{\text{成功}} = 1 - (1-p)^{n}

消费者幂等

同一个包裹被快递员送了两次,你会付两次钱吗?不会,因为你心里有一本“收件登记本”——第一次签收时已经记下“这台咖啡机收了”。第二次快递员再来,你翻一下本子,直接告诉他“收过了”。消费者处理消息也要有这本“登记本”,这就是幂等:同一个操作执行一次和一万次,效果完全一样。

重复消息从哪来?最常见的就是上一节说的:生产者重发、Broker 重试、网络超时后客户端重连,都会导致同一条消息被投递多次。要让重复无害,关键在于给每条消息一个全局唯一 ID,比如“订单号 + 事件序号”。消费者处理前先查“已处理集合”:ID 不在集合里,就执行真正的业务逻辑并把 ID 记进去;ID 已经在集合里,直接跳过。

实现幂等有两种常见姿势。存储去重:用数据库唯一索引或 Redis 的 SETNX 做登记本,简单直接;状态幂等:像订单这种有状态的业务,只允许“待支付 → 已支付”这种合法流转,重复消息走到非法状态就自动放弃。有了登记本,快递员送几次你都不慌。

result(id)={process(id),id∉Sskip,id∈S\displaystyle \text{result}(id) = \begin{cases} \text{process}(id), & id \notin S \\ \text{skip}, & id \in S \end{cases}
S 是“已处理 ID 登记本”,消息 ID 没登记过就处理,登记过就跳过,重复变成无害操作。
  1. 已处理集合S 是消费者维护的“收件登记本”,记录所有已经成功处理过的消息 ID。
    S={id1, id2, …}S = \{id_1,\,id_2,\,\ldots \}
  2. 首次命中消息 ID 不在 S 中,说明是第一次来,执行真正的业务处理,并把 ID 写进登记本。
    id∉S  ⇒  process(id),  S←S∪{id}id \notin S \;\Rightarrow\; \text{process}(id),\; S \leftarrow S \cup \{id\}
  3. 重复命中ID 已经在 S 里,说明之前处理过,直接跳过,不重复执行业务逻辑。
    id∈S  ⇒  skip(id)id \in S \;\Rightarrow\; \text{skip}(id)

消息持久化

中转仓每天经手成千上万个包裹,光靠脑子记绝对不行——得有一排排货架和一本总账。Broker 也是一样:收到消息就写进磁盘(落盘),并存一份追加式日志当“账本”。这样即使 Broker 宕机、断电重启,也能按账把货一件件找回来。

落盘有个性能与安全的权衡,这就是刷盘策略。同步刷盘:每条消息都强制写盘成功后才返回 ACK,最安全但最慢;异步刷盘:先把消息攒在内存里,凑一批再写盘,性能高,但机器突然断电可能丢掉最后一批。更高级的保障是多副本复制:同一份消息复制到多台机器上,一台宕了还有备份。Kafka 的 ISR(同步副本集合)就是这个思路——只有足够多的副本同步成功,才向生产者确认“已提交”。

副本确认的判定写成公式就是:

账本加备份,仓库失火也不怕。但总有一些包裹是“硬骨头”,怎么都送不出去——它们需要一条专门的处理通道。

committed=(∑i=1Nai≥R),ai∈{0, 1}\displaystyle \text{committed} = \left( \sum_{i=1}^{N} a_i \ge R \right), \quad a_i \in \{0,\,1\}
N 个副本中,至少 R 个副本同步成功,这条消息才算真正“入账”,否则不确认。
  1. 副本状态a_i 表示第 i 个副本的同步结果,1 表示“已同步”,0 表示“还没同步”。
    ai={1同步成功0未同步a_i = \begin{cases}1 & \text{同步成功}\\ 0 & \text{未同步}\end{cases}
  2. 同步总数把所有副本的 a_i 加起来,就是当前成功同步的副本数量。
    ∑i=1Nai=已同步的副本数\sum_{i=1}^{N} a_i = \text{已同步的副本数}
  3. 提交判定当同步成功数达到阈值 R,Broker 才认为消息持久化完成,才向生产者回复 ACK。
    ∑i=1Nai≥R  ⇒  committed\sum_{i=1}^{N} a_i \ge R \;\Rightarrow\; \text{committed}

死信策略

有些包裹收件人地址写错了,或者收件人拒收。快递公司不会让它在传送带上无限循环占着位置,而是把它拣出来,放进“问题件仓库”集中处理。消息系统里也有这个“问题件仓库”,叫死信队列(DLQ)。

一条消息被消费者反复处理失败,重试到第 3 次、第 5 次都还是失败,如果继续重试,正常消息就会被它堵住。所以系统设定一个重试上限 R_max,一旦超过上限,Broker 就把消息从正常队列中移出,转存到死信队列。死信不是垃圾站,而是“问题件待办区”——运维人员需要监控告警,查看死信里的消息为什么失败(反序列化错误?业务逻辑 bug?),修复之后再把消息重放回正常流程。

死信的判定规则非常清晰:

问题件集中管理,正常的包裹流才不会被堵住。但如果某个包裹其实送错了地方、搞错了内容,我们还有最后一招——查物流记录,重新派送。

action(r)={retry,r≤Rmax⁡dead_letter,r>Rmax⁡\displaystyle \text{action}(r) = \begin{cases} \text{retry}, & r \le R_{\max} \\ \text{dead\_letter}, & r > R_{\max} \end{cases}
r 是已重试次数,没超上限就继续试,超了上限就转到死信队列,防止一条坏消息堵死整条链路。
  1. 重试计数r 是这条消息已经重试的次数,R_max 是系统配置的最大重试次数。
    r∈{0, 1, 2, …},Rmax⁡>0r \in \{0,\,1,\,2,\,\ldots\}, \quad R_{\max} > 0
  2. 重试条件只要累计重试次数没有超过上限,Broker 就继续把消息投递给消费者。
    r≤Rmax⁡  ⇒  retryr \le R_{\max} \;\Rightarrow\; \text{retry}
  3. 转入死信一旦重试次数超过上限,消息被移入死信队列,等待人工分析和干预。
    r>Rmax⁡  ⇒  dead_letter queuer > R_{\max} \;\Rightarrow\; \text{dead\_letter queue}

消息回溯

你发现包裹被送错了小区,快递公司怎么查?靠的是每个节点留下的物流轨迹记录,一单一单往回对。消息系统的回溯原理类似:Broker 把消息数据保留一段时间(比如 3 天),并且记录消费者消费到哪一条(消费位点)。当业务出问题需要重新处理历史消息时,管理员把消费位点往回调,消费者就会从旧位置重新拉取消息,从头再处理一遍。

回溯有两种常见方式。按时间戳回溯:把位点拨到“今天上午 10 点”,消费者就从那一刻的消息开始重放;按偏移量回溯:精确回退到某一条消息的序号,适合定位到具体的坏数据。回溯在 AI 工程里特别有用——模型版本回滚后,需要用旧特征重新喂数据;风控规则调整后,需要重放历史事件重新打分。这相当于系统的“后悔药”。

回溯的操作本质是一次位点回退:

有了物流轨迹,送错了也能追得回来。至此,寄件要回执、仓库有账本、派件有登记、问题件有仓库、记录可追溯——一套完整的“不丢不重”快递工程就齐了。

positionnew=positionstored−Δ\displaystyle \text{position}_{\text{new}} = \text{position}_{\text{stored}} - \Delta
position_stored 是当前的消费进度,Δ 是回退的偏移量或时间窗口,把进度拨回过去,让消费者从头再来。
  1. 存储位点position_stored 是 Broker 记录的当前消费位置,类似物流轨迹里“送到哪个站了”。
    positionstored=最后一条已消费消息的偏移量\text{position}_{\text{stored}} = \text{最后一条已消费消息的偏移量}
  2. 回退量Δ 是往回退的距离,可以是消息条数,也可以是时间长度。
    Δ=回溯的偏移量或时间窗\Delta = \text{回溯的偏移量或时间窗}
  3. 计算新位点从存储位点减去 Δ,得到新的消费起点,消费者从该位置重新拉取并处理。
    positionnew=positionstored−Δ\text{position}_{\text{new}} = \text{position}_{\text{stored}} - \Delta

AI 应用场景

电商订单事件通知管道

阶段:事件驱动集成

解决的问题:用户支付成功后,需要同步通知积分系统、库存系统、短信服务。传统做法是用数据库轮询,每秒钟查一次“有没有新支付”,实现复杂、延迟高、且轮询到一半服务重启还容易重复发送通知。引入消息队列后,支付服务作为生产者发送“支付成功事件”,各下游系统作为消费者订阅,生产确认保证事件不丢,消费者幂等保证重复推送时用户不会收到两条相同的短信。

具体案例:某电商平台将订单服务与积分服务解耦,订单支付成功后发送 order.paid 事件。网络抖动导致事件重复投递时,积分系统根据 orderId + 事件版本号 做幂等去重,用户积分只加一次。高峰期每秒数千笔订单,消息全部可靠落盘,无一丢失。

AI 训练数据采集管道

阶段:数据采集与预处理

解决的问题:AI 模型训练依赖海量埋点日志,这些日志从 App、服务端实时采集后进入数据管道。如果采集链路丢消息,训练数据的分布就会出现偏差,模型效果变差。使用带持久化和多副本的消息集群,配合生产确认,日志一到 Broker 就落盘,采集端不确认不罢休,从源头保证“一条都不能少”。

具体案例:某推荐系统团队搭建日志管道,每小时采集数亿条用户行为。某次机房断电恢复后,靠多副本数据完整找回,训练任务无缝继续;消费者端用 日志ID 去重,重试导致的重复日志被自动过滤。

实时风控特征更新

阶段:实时推理特征服务

解决的问题:风控模型的特征(如用户最近 5 分钟的登录次数)由实时事件流更新。特征消息一旦丢失,模型就会基于过期特征做出错误放行。通过生产者确认 + 消费幂等,保证每条行为事件都准确反映到特征库,同时重复事件不会把计数器重复累加。

具体案例:某支付风控系统订阅登录事件流,消费者收到事件后按 userId + 事件类型 做幂等更新。一次网络分区导致部分事件重放,但特征计数器没有翻倍,风控评分保持准确。

Python 演示

python71 行
# 消息可靠投递模拟:生产者确认 + 持久化 + 消费幂等 + 死信import time  class Broker:    """模拟消息队列:负责存储、投递、重试与死信"""     def __init__(self):        self.store = {}       # 持久化存储:msg_id -> body        self.unacked = []     # 未确认消息队列        self.retry_map = {}   # 每条消息的重试次数        self.dlq = []         # 死信队列        self.max_retry = 3    # 重试上限     def accept(self, msg_id, body):        """生产确认:消息落盘后返回 ACK"""        self.store[msg_id] = body        self.unacked.append(msg_id)        return True     def deliver(self, consumer):        """投递一轮:确认成功则出队,失败则计重试,超限进死信"""        for mid in list(self.unacked):            if self.retry_map.get(mid, 0) > self.max_retry:                self.dlq.append(mid)                self.unacked.remove(mid)                continue            if consumer.consume(mid, self.store[mid]):                self.unacked.remove(mid)            else:                self.retry_map[mid] = self.retry_map.get(mid, 0) + 1  class IdempotentConsumer:    """消费者:已处理 ID 集合保证幂等,bad 前缀消息模拟失败"""     def __init__(self):        self.processed = set()  # 已处理的唯一 ID 登记本     def consume(self, mid, body):        if mid in self.processed:   # 重复消息,幂等跳过            return True        if body.startswith("bad"):            return False            # 模拟业务处理失败        self.processed.add(mid)        return True  def main():    broker = Broker()    consumer = IdempotentConsumer()     # 生产者发送 5 条消息,其中 msg-3 模拟业务处理失败    for i in range(1, 6):        body = "bad-3" if i == 3 else "payload-%d" % i        broker.accept("msg-%d" % i, body)     # 循环投递,直到队列清空或全部进死信    rounds = 0    while broker.unacked and rounds < 10:        broker.deliver(consumer)        rounds += 1        time.sleep(0.05)     print("成功消费:", sorted(consumer.processed))    print("死信队列:", broker.dlq)    print("投递轮数:", rounds)  if __name__ == "__main__":    main()
目的:用不到 100 行模拟了消息可靠投递的完整闭环——生产确认落盘、消费确认出队、失败重试、超限转死信、幂等去重,运行后观察 成功消费 和 死信队列 的输出即可理解各机制如何协作。
  1. self.store[msg_id] = body等价于“中转站收货入库”,消息落盘后 Broker 才返回 ACK。
  2. if consumer.consume(mid, self.store[mid]):投递消息并等待消费者确认,确认成功才从待确认队列移除。
  3. self.retry_map[mid] = self.retry_map.get(mid, 0) + 1消费者没有确认,重试次数加一,等待下一轮再投。
  4. if self.retry_map.get(mid, 0) > self.max_retry:超过重试上限,消息转入死信队列,不再占用正常队列。
  5. if mid in self.processed:幂等检查:登记本里已有该 ID,直接跳过业务逻辑。
  6. body.startswith("bad")模拟消息内容有问题导致消费者处理失败,触发重试与死信流程。

常见误区

误区很多人以为只要设置了 ACK 确认,消息就绝对安全了,不用再管别的。我只要发消息等回执就行,对吧?

正解生产者确认只保证“消息到达了 Broker 并落盘”,只覆盖了寄件到中转这一段。中转站把包裹送给收件人这段路还没保障:消费者收到后处理到一半崩溃、或者 ACK 在返回途中丢失,消息都会再次投递或永久无人处理。必须有消费确认、持久化和死信策略配合,整条链路才算可靠。

为何常见:因为初学者最容易接触到的就是发送端的确认 API,误以为它会贯穿全链路,忽略了下游环节也有丢失风险。

误区给数据库表加个唯一索引,重复消息插入时报错就完事了,这不就是幂等吗?

正解唯一索引只是“存储去重”的一种实现,而且它只能挡住数据库层的重复写。真实业务里还有缓存更新、外部 API 调用、消息转发等副作用,唯一索引管不到。更完整的幂等要结合业务唯一 ID、状态机流转和外部调用去重,保证所有副作用的最终效果一致。

为何常见:因为很多教程把“唯一索引防重复”当作幂等的典型案例,容易让人把手段当成整个方案。

误区消息只要写入磁盘就不会丢了,所以刷盘越频繁越好,最好每条消息都同步刷。

正解同步刷盘确实最安全,但性能代价极高,吞吐量可能下降一个数量级。真正的工程方案是组合拳:异步刷盘攒批写盘提高性能,再靠多副本跨机器冗余兜底——一台机器断电丢了内存数据,另一台机器还有副本。可靠性是“刷盘 + 副本 + 确认机制”的系统设计,不是单点动作。

为何常见:因为“写磁盘”听起来像是一锤定音的物理保障,容易忽略断电瞬间数据可能还停留在操作系统缓存里这一事实。

误区死信队列就是装失败消息的垃圾桶,只要消息进了死信,问题就自动解决了。

正解死信队列是“问题件待办区”,不是终点。如果没人监控、没人分析、没人重放,死信里的消息就相当于静默丢失——业务影响可能非常严重。必须配置告警,分析失败原因,修复后用工具把死信重新投递回正常队列。

为何常见:因为从现象上看,消息确实是“消失了”,系统也没有报错,容易让人误以为一切正常。

误区需要重放历史消息时,把生产者那边的发送代码再跑一遍不就行了?

正解回溯是 Broker 侧的操作:把消费位点回退,让消费者重新拉取已被存储的历史数据,并不经过生产者,也不会重新触发生产确认和业务事务。如果跑一遍生产者代码,会产生全新的消息 ID,还可能重复扣款、重复计费。回溯的语义是“过去的数据重新读一遍”,而不是“现在的代码再发一遍”。

为何常见:因为两者的结果看起来都是“消息又出现了一次”,容易混淆消息的来源是生产者还是 Broker 的历史存储。

面试题

Q1. 生产者确认的 ACK 通常在什么时候返回?

单选题 · mid

  • A.消息刚发出时
  • B.消息落盘成功后← 正确答案
  • C.消费者消费成功后
  • D.消费者确认结束后

答案:B。ACK 是 Broker 向生产者确认“消息已收到并持久化”,时机在落盘成功后。A 只是发送动作完成,C 和 D 属于消费确认环节,与生产者确认不是一回事。这就像快递站收到包裹、登记入库后才会给寄件人回执单。

Q2. “至少一次”投递语义的最大特点是什么?

单选题 · easy

  • A.消息一定不丢失
  • B.可能出现重复消息← 正确答案
  • C.消息完全有序
  • D.消费速度最快

答案:B。至少一次语义保证“不丢”,但允许重复。它通过重试机制实现,网络超时或 ACK 丢失都会导致同一条消息被投递多次。所以它通常要配合消费者幂等一起使用,才能让重复无害化。

Q3. 哪些环节可能导致消息丢失?

多选题 · mid

  • A.生产者网络闪断← 正确答案
  • B.中转宕机未落盘← 正确答案
  • C.消费者崩溃未确认← 正确答案
  • D.消息没有设置优先级

答案:ABC。三个环节都可能丢:生产者发到一半网络断了消息根本没到 Broker;Broker 宕机时消息还在内存没落盘;消费者处理到一半崩溃没返回确认,Broker 不知道结果。消息优先级与丢失无关,属于额外属性。

Q4. 实现消费幂等最常用的方案是什么?

单选题 · mid

  • A.消费前随机延时重试
  • B.消息唯一ID加去重表← 正确答案
  • C.消费时无限次重发
  • D.使用多个消费者并行处理

答案:B。最常用的是给消息一个全局唯一 ID,消费者处理前查“已处理登记本”,命中就跳过。随机延时和并行处理不能解决重复问题,无限重发反而加剧重复。就像快递员重复送货,你翻一下收件登记本就能拒收。

Q5. 下列哪些手段可以提升 Broker 持久化能力?

多选题 · hard

  • A.开启多副本同步复制← 正确答案
  • B.消息写盘后才返回确认← 正确答案
  • C.增加消费端重试次数
  • D.配置合理的刷盘策略← 正确答案

答案:ABD。多副本同步复制让数据跨机器冗余,写盘后确认保证断电不丢,合理的刷盘策略在性能和可靠性之间取得平衡。消费端重试次数只影响投递环节,与 Broker 的持久化无关。

Q6. 死信队列的主要作用是什么?

单选题 · easy

  • A.存放重试超限的消息← 正确答案
  • B.加速消息消费速度
  • C.保存所有历史消息
  • D.替代主消息队列

答案:A。死信队列专门接收重试超过上限、始终处理失败的消息,相当于快递公司的“问题件仓库”。它不是加速器也不是历史归档,更不会替代主队列,而是等待人工介入分析和重放。

Q7. 消息回溯能力的核心基础是什么?

单选题 · mid

  • A.Broker保留历史数据← 正确答案
  • B.消费者启动更快
  • C.生产确认更频繁
  • D.消息压缩率更高

答案:A。回溯要求 Broker 在一定时间窗口内保留已消费的历史消息,并记录消费位点,才能把位点拨回去重新读取。启动速度、确认频率和压缩率都不构成回溯的基础。就像物流公司要留足各节点的签收记录才能回查。

Q8. 实现“精确一次”投递通常需要哪些配合?

多选题 · hard

  • A.生产方做发送去重← 正确答案
  • B.Broker端事务协调← 正确答案
  • C.消费方做幂等处理← 正确答案
  • D.省略生产方确认流程

答案:ABC。精确一次是系统工程:生产方去重防止重复发,Broker 用事务或幂等机制保证存储层不重,消费方幂等保证副作用不重。省略生产方确认会让消息发出后无法确认状态,反而更容易丢失。

Q9. 消费者处理成功但 ACK 丢失,会发生什么?

单选题 · mid

  • A.消息被自动删除
  • B.Broker会重新投递← 正确答案
  • C.消息进入死信队列
  • D.消费者立即停机

答案:B。Broker 没收到 ACK 就认为消息还没消费完,会重新投递。这就是“至少一次”语义下重复消息的来源之一。消费者必须靠幂等接住这次重复。死信是重试超限后的去处,不是一次 ACK 丢失就触发。

Q10. 一套完整可靠投递方案通常包含哪些内容?

多选题 · easy

  • A.生产者确认机制← 正确答案
  • B.消息持久化存储← 正确答案
  • C.消费者幂等处理← 正确答案
  • D.关闭消息队列日志

答案:ABC。完整方案覆盖全链路:生产端等 ACK 确认送达,Broker 持久化保证不丢,消费端幂等保证重复无害。关闭日志反而会让问题无法排查,与可靠性目标背道而驰。