RabbitMQ 常见于验证码、短信、邮件、订单异步处理、缓存同步、分布式事务最终一致和削峰填谷。只要把核心链路从"同步调用"改成"发消息",第一个必须回答的问题就是:消息会不会丢,重复消费怎么办?
一句话概括:RabbitMQ 可靠性要沿着生产者、交换机、队列、消费者四段链路排查;生产者确认保证消息到 MQ,持久化保证消息在队列里尽量不丢,消费者 ACK 保证业务处理成功后再删除消息,幂等设计兜住重复投递。

publisher confirm
routing key 绑定
投递消息
发送失败
路由失败
宕机风险
处理失败
生产者
Exchange
Queue
消费者
记录日志或入库重发
publisher return
交换机/队列/消息持久化
ACK/NACK + 重试 + 异常队列
消息会丢在哪里
课件里把 RabbitMQ 消息丢失拆成四个位置:
| 丢失位置 | 典型原因 | 解决机制 |
|---|---|---|
| 生产者到交换机 | 网络抖动、MQ 不可用 | publisher confirm |
| 交换机到队列 | 路由键错误、没有绑定队列 | publisher return 或 mandatory |
| 队列中 | MQ 宕机、队列或消息没持久化 | 交换机、队列、消息持久化 |
| 消费者处理阶段 | 消费者宕机、业务异常、提前删除消息 | 消费者 ACK、重试、异常交换机 |
所以回答"RabbitMQ 如何保证消息不丢"时,不要只说一个 ACK。生产端、Broker 存储端、消费端都要覆盖到。
生产者确认机制
RabbitMQ 提供 publisher confirm,生产者把消息发给 MQ 后,MQ 会返回确认结果:
| 结果 | 含义 | 常见处理 |
|---|---|---|
ack |
消息已经被 MQ 接收 | 正常结束 |
nack |
消息没有成功写入 MQ | 记录日志、重试或入库补偿 |
return |
消息到了交换机,但没有路由到队列 | 检查 exchange、routing key、binding |
确认机制解决的是"生产者以为发出去了,实际上 MQ 没收到"的问题。
补偿任务 Queue Exchange 生产者 补偿任务 Queue Exchange 生产者 alt [写入成功] [写入失败] [路由失败] 发送消息 根据 routing key 路由 confirm ack confirm nack 保存失败消息,等待重发 return 消息 记录 exchange/routing key 异常
消息失败后不要无限递归重发。更稳的方式是:失败消息先落库或写日志,定时任务扫描重发,发送成功后删除补偿记录。
confirm 和 return 的边界
这里有一个容易混淆的点:publisher confirm 和 publisher return 不是同一层保障。
| 机制 | 关注点 | 能说明什么 | 不能说明什么 |
|---|---|---|---|
publisher confirm |
生产者到 RabbitMQ | Broker 是否接收并处理了发布请求 | 消费者是否处理成功 |
publisher return |
交换机到队列 | mandatory 消息没有路由到任何队列 | Broker 是否持久化成功 |
| 消费者 ACK | RabbitMQ 到消费者 | 消费者是否成功处理消息 | 生产者是否发送成功 |
能
不能且 mandatory=true
Producer
Exchange
能否路由到 Queue
Queue 接收消息
basic.return 返回生产者
publisher confirm ack
所以生产端完整做法通常是:开启 confirm 判断消息是否被 Broker 接收;同时对关键消息开启 mandatory/return,发现路由失败时记录配置问题或进入补偿。
消息持久化
生产者确认只能说明消息进了 MQ,不能说明 MQ 重启后消息还在。要让队列里的消息尽量不丢,需要同时打开三类持久化:
| 持久化对象 | 作用 |
|---|---|
| 交换机持久化 | MQ 重启后交换机仍然存在 |
| 队列持久化 | MQ 重启后队列仍然存在 |
| 消息持久化 | 队列里的消息可以落盘保存 |
Spring AMQP 中可以这样声明:
java
@Bean
public DirectExchange simpleExchange() {
// durable=true 表示交换机持久化,autoDelete=false 表示没有队列绑定时不自动删除。
return new DirectExchange("simple.direct", true, false);
}
@Bean
public Queue simpleQueue() {
return QueueBuilder.durable("simple.queue").build();
}
Message msg = MessageBuilder
.withBody(message.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
这里要注意一个边界:持久化不是"绝对不丢"。如果消息刚写入内存还没刷盘,机器突然故障,仍然可能有极小概率丢失。工程上通常配合生产者确认、集群高可用和补偿任务一起使用。
消费者确认机制
消费者确认解决的是"消息已经投递给消费者,但业务还没处理完,消费者就挂了"的问题。
RabbitMQ 收到消费者的 ack 后才会删除消息。如果没有收到 ack,消息会重新进入可投递状态,交给其他消费者处理。
Spring AMQP 常见三种确认模式:
| 模式 | 含义 | 风险 |
|---|---|---|
manual |
业务代码手动调用 API 发送 ACK | 最灵活,但代码复杂 |
auto |
Spring 根据 listener 是否抛异常自动 ACK/NACK | 项目里常用 |
none |
投递后立即认为成功 | 消费者宕机时容易丢消息 |
成功
异常
能
不能
Queue 投递消息
消费者执行业务
业务是否成功
返回 ACK
MQ 删除消息
返回 NACK 或抛异常
是否还能重试
本地重试或重新入队
投递到异常交换机
课件里的推荐组合是:消费者开启自动确认,由 Spring 判断 listener 是否正常结束;业务异常时先本地重试,多次失败后投递到异常交换机,交给人工或补偿任务处理。
prefetch 和毒丸消息
消费端可靠性还要关注两个工程问题:消费者一次拿多少消息,以及失败消息会不会反复拖垮系统。
prefetch 表示一个消费者最多可以同时持有多少条未 ACK 的消息。它像一个滑动窗口:窗口满了,RabbitMQ 就先不继续推消息,等消费者 ACK 后再投递新的消息。
否
是
Queue
Consumer
未 ACK 消息窗口
是否达到 prefetch
继续投递
暂停投递,等待 ACK
| 设置 | 影响 |
|---|---|
prefetch 太小 |
消费者吞吐可能上不去 |
prefetch 太大 |
消费者内存压力大,失败时会有大量未确认消息重投 |
prefetch=0 |
表示不限制,普通业务不建议随意使用 |
毒丸消息指的是一条消息本身有问题,比如数据格式不合法、业务状态永远无法满足,导致消费者每次处理都失败。如果一直 requeue=true,它会反复回到队列,形成失败重试循环。
更稳的处理方式是:本地重试有限次数;仍然失败后 reject/nack 且不重新入队;再把消息投递到死信队列或异常队列,交给人工排查。
重复消费不是异常,而是必须设计
只要有 ACK、重试、网络抖动和消费者重启,就一定可能出现重复消费。
比如消费者业务已经处理成功,但 ACK 回给 MQ 的过程中网络断了。MQ 没收到 ACK,就会认为消息没有处理成功,后续重新投递。此时业务必须能识别"这条消息已经处理过"。
数据库 消费者 Queue 数据库 消费者 Queue 投递消息 msg-1001 执行业务成功 ACK 丢失 重新投递 msg-1001 根据业务唯一键判断已处理 ACK
幂等方案通常有三类:
| 方案 | 适合场景 | 说明 |
|---|---|---|
| 消息唯一 ID | 通用 MQ 消费 | 消费前检查 messageId 是否已处理 |
| 业务唯一标识 | 支付、订单、文章、优惠券 | 用支付单号、订单号等天然唯一键去重 |
| 锁或唯一索引 | 并发写入风险高 | 数据库唯一索引、乐观锁、悲观锁、分布式锁 |
更推荐优先使用业务唯一标识。比如支付回调消息重复消费时,不要只依赖 MQ 的消息 ID,而要检查支付单号对应的业务状态是否已经完成。
面试回答模板
可以这样答:
RabbitMQ 保证消息可靠要从三段链路说。第一,生产者到 MQ 之间开启 publisher confirm,消息发送失败时通过回调记录日志或落库重发;confirm 只说明 Broker 侧处理了发布请求,如果消息到了交换机但没有路由到队列,还要配合 mandatory 和 return 机制排查 routing key 和绑定关系。第二,Broker 侧要开启交换机、队列和消息持久化,避免 MQ 重启后消息丢失。第三,消费者侧开启 ACK 机制,业务处理成功后再确认,同时设置合理 prefetch,避免消费者被过多未确认消息压垮。失败消息要有限重试,多次失败后进入异常队列或死信队列,避免毒丸消息反复重投。最后还要说明 MQ 只能尽量保证至少一次投递,重复消费一定可能出现,所以业务必须做幂等,比如用消息 ID、订单 ID、支付 ID 或数据库唯一索引去重。
小结
RabbitMQ 可靠性不是单点配置,而是一套组合拳:
生产者确认
Broker 持久化
消费者 ACK
prefetch 限流
失败有限重试
异常队列
消费幂等
把这条链路讲完整,比单纯背 confirm、ack、durable 更像真实项目里的回答。