MESSAGE QUEUE · 可靠交付专题

MQ 消息可靠性:一条消息在哪些位置会丢

MQ(Message Queue,消息队列)是在生产者和消费者之间暂存、转发消息的系统。可靠交付不是打开一个配置开关,而是逐段确认“生产者是否交给 Broker、Broker 是否安全保存、消费者是否完成业务”。Broker 是负责接收、保存和投递消息的服务端。

厂商中立主线RabbitMQ 4.x 示例重点:确认 · 持久化 · 重试 · 重复
一、为什么需要消息队列:先让下单和发短信分开

用户下单后,需要扣库存、发短信、积积分。若下单接口同步等待全部步骤,任一慢服务都会拖住用户。消息队列让下单服务先提交核心结果,再把后续工作作为消息交给消费者异步处理。

解决的问题

异步、削峰和解耦:生产者不必知道消费者何时上线,突发消息可在队列里排队。

新增的问题

消息可能丢、重复、延迟、乱序或积压。系统从一次本地调用变成跨网络的多段责任转移。

分析方法:每条箭头都可能断。不要笼统问“MQ 会不会丢”,要问丢在发送前、网络中、Broker 内、投递后还是业务提交时。
二、全链路故障地图:四个断点逐个封住

Publisher Confirm(发布确认)是 Broker 接管消息后返回给生产者的回执;Consumer Acknowledgement(消费确认,简称 ack)是消费者完成业务后返回给 Broker 的回执。它们方向相反,各自只覆盖一段责任转移。图中的 order.created 是“订单已创建”事件名,messageId=88 是这条消息用于识别重发的唯一编号。

一条 order.created 消息:数据、确认和责任怎样移动 ① 业务数据库订单已提交② 生产者发送 messageId=88③ Broker持久队列 / 副本④ 消费者写入积分库 断点 A断点 B断点 C断点 D 订单提交后进程崩溃消息尚未发送本地消息表 / 事务消息 发送或回执丢失生产者不知道 Broker 状态确认 + 超时重发 Broker 节点故障仅在内存或副本不足持久化 + 复制 消费者先确认后崩溃Broker 已删除,业务未完成业务成功后手动确认 Publisher Confirm:Broker 接管后回生产者 Consumer Ack:业务完成后回 Broker 任一确认丢失都可能触发重发,所以可靠通常伴随重复消息 ID=88 必须让消费者做到幂等
最应记住:生产确认和消费确认互相独立,只覆盖各自的一段;图上每个断点都要有对应机制。常见误读是“Broker 开了持久化就全链路绝不丢”。
三、生产者到 Broker:确认没收到时为什么必须重发

Publisher Confirm(发布确认)是 Broker 告诉生产者“我已经按当前队列规则接管这条消息”的回执。网络断开时,生产者可能无法区分“消息没到”和“消息到了但确认没回来”。

动作防住什么仍可能发生
设置消息唯一 ID让重复可识别本身不阻止重复
等待发布确认发现未被 Broker 接管的消息确认丢失导致重发和重复
不可路由返回/mandatory发现交换机没有匹配队列不代表消费者完成业务
有界重试与退避应对瞬时网络故障长故障需落盘、告警和人工处置
两种错误都要处理:Broker 明确否认时可立即失败;超时属于结果未知,重发更安全但可能重复。因此生产者可靠性设计天然要求消费者幂等。
四、Broker 内部:持久化、复制与确认时机必须配套

持久化表示消息和队列元数据按配置写入可恢复存储;复制表示多个节点保存消息副本。单机写盘防进程重启,多副本应对节点损坏,但都会增加写入延迟、磁盘和网络成本。

持久队列

队列自身在重启后仍存在。只让队列 durable,不等于每条消息都按持久方式发布。

持久消息

消息要求落入可恢复存储。真正保证取决于队列类型与确认时机。

复制队列

例如 RabbitMQ quorum queue 使用多数副本思路提高节点故障下的数据安全。

容量也是可靠性:磁盘满、内存告警、队列达到长度限制、不可路由和错误过期策略都可能造成拒绝或丢弃。必须监控未确认消息、积压、磁盘和副本健康。
五、Broker 到消费者:确认要放在业务成功之后

Consumer Acknowledgement(消费确认,简称 ack)是消费者告诉 Broker“这条投递已经处理成功,可以删除”。自动确认通常在消息刚送出时就转移责任,消费者随后崩溃可能直接丢业务。

同一条消息,只改变确认时机 Broker 投递 messageId=88 分支 A:自动/提前确认先 ack → Broker 删除业务写库前崩溃 分支 B:手动确认先完成业务写库再 ack 消息没了,业务也没完成结果:丢失 若 ack 丢失,Broker 再投结果:不丢,但可能重复 本例走分支 B:把丢失风险换成可治理的重复风险。
最应记住:业务成功后再 ack 能避免“确认了却没做完”,但业务成功后、ack 前崩溃会重复投递。常见误读是认为手动确认等于恰好一次。
六、三种交付语义:名字说的是次数边界,不是魔法
语义白话含义常见实现业务要求
At-most-once(至多一次)可能 0 次或 1 次,不重试提前确认、失败不重发能接受丢失
At-least-once(至少一次)一定努力送到,但可能多次确认、超时重发、未 ack 重投消费者幂等
Exactly-once(恰好一次效果)最终业务结果只发生一次特定系统内事务或幂等写入必须说明覆盖边界
面试纠错:跨数据库、外部 API 和任意消息系统的“端到端绝对恰好一次”通常不能靠 MQ 单独承诺。更实用的目标是至少一次交付,加消费者幂等,得到业务上的一次效果。
七、本地事务与发消息之间:最隐蔽的双写断点

订单提交与消息发布属于两个系统。先提交订单再发消息,可能在中间崩溃;先发消息再提交订单,消费者可能收到一个后来回滚的订单。

  1. 在同一个数据库事务中写订单和 outbox(本地消息表)记录。
  2. 事务提交后,独立发布器扫描未发送记录。
  3. 发布到 Broker 并收到确认后,把 outbox 标记为已发送。
  4. 若标记前崩溃会重复发布,因此消息 ID 和消费者幂等不可少。
BEGIN;
INSERT INTO orders(id, status) VALUES (42, 'PAID');
INSERT INTO outbox(message_id, topic, payload, status)
VALUES ('msg-88', 'order.paid', '{"orderId":42}', 'NEW');
COMMIT;
代价:本地消息表把“订单与待发送记录”变成一个本地事务,但引入扫描、积压、清理和重复发布。事务消息、CDC(Change Data Capture,变更数据捕获)也有各自基础设施假设。
八、坑点、面试话术与应用场景

高频坑点

  • 发送 API 不报错就认为 Broker 已持久化。
  • 只开持久化,不等待发布确认。
  • 消费者先 ack,再提交业务数据库。
  • 无限重试,不设退避、上限和死信出口。
  • 消息没有稳定 ID,重复到来无法识别。
  • 只关注“不丢”,完全不监控积压和处理延迟。
90 秒回答:消息可靠性要按链路分段。业务数据库到生产者之间用事务消息或本地消息表封住双写断点;生产者到 Broker 用唯一消息 ID、发布确认、不可路由返回和有界重试;Broker 内用合适的持久化、复制和容量策略;消费者在业务提交成功后手动 ack。确认丢失会让消息重发,所以可靠交付通常选择至少一次,并要求消费者幂等。任何“恰好一次”都要说明它覆盖哪些系统和失败边界。

应用场景

订单事件、通知、日志、异步任务与削峰适合消息队列,因为生产者和消费者可以异步推进。同步调用必须立刻得到结果、强事务又无法接受补偿的流程,不应为了“解耦”硬塞消息队列。

资料依据