RabbitMQ
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
本文以 RabbitMQ 4.1、AMQP 0-9-1 为说明范围;Java API 名称采用 RabbitMQ Java Client 的写法。旧版经典镜像队列仅作为历史概念介绍,不适用于 4.x。
1、为什么要用MQ
在分布式系统下具备异步、削峰、解耦等功能。
- 支持持久化,但可靠保存需要结合队列类型、消息持久化和发布确认,并非放入 MQ 就不会丢失;
- 实现消费者和生产者之间的解耦;
- 高并发请求先进入队列,再由有限数量的消费者处理,可以削峰并保护数据库;MQ 不会自动把所有访问串行化,吞吐仍受消费者与下游容量限制;
- 可以实现异步下单:先返回“排队中”,后台处理后再更新订单结果,需要定义失败补偿与用户查询方式。
2、RabbitMQ的组成成分和工作原理
- 生产者(Producer)、信道(Channel)、交换机(Exchange)、绑定(Binding)、队列(Queue)、消费者(Consumer)。Channel 是连接上的逻辑通信通道。
- 生产者通过信道把消息发送给交换机,交换机根据类型、绑定及 routing key 路由到零个或多个队列,消费者从队列接收消息。交换机负责路由,不负责存储消息;手动确认时,已投递消息在收到 ACK 前仍属未确认消息。消息也可能因 TTL 或长度限制被移除。
3、常用的交换机有哪几种
- Direct:routing key 与 binding key 精确匹配,所有匹配的队列都会收到消息,并非只能一对一发送。
- 默认交换机是名称为
""的特殊 direct 交换机,队列声明后自动以队列名绑定它,因此可以用队列名作为 routing key 发送。 - Fanout:广播,将消息交给所有绑定到交换机的队列,忽略 routing key。
- Topic:按 binding key 模式匹配 routing key;单词以
.分隔,例如item.insert。#:匹配零个或多个词,如item.#可匹配item、item.insert。*:恰好匹配一个词,如item.*可匹配item.insert,不能匹配item.stock.update。
- Headers:按消息头及绑定参数匹配,而不是按 routing key 匹配。
参见官方 Exchanges。
4、如何保证消息不丢失
网络中断、进程崩溃、错误路由、提前确认以及过期策略都可能导致业务消息缺失,应分别处理各段链路。
- 生产端:调用
channel.confirmSelect()开启 publisher confirms,跟踪当前信道上的发布序号,处理 nack、超时与断线后的重试。confirm 表示 broker 接受了发布,不表示消费者已完成业务;无法路由的消息也可能获得 confirm。需要检测未路由消息时,发布设置mandatory=true并处理basic.return。 - Broker 端:需要重启后保留的队列声明为 durable,消息设置持久化属性
deliveryMode=2,需要保留的交换机也应 durable。交换机持久化保存的是拓扑,不会替代消息落盘。重要消息结合 confirm 与复制队列,单靠持久化声明仍不足以覆盖所有故障。 - 业务数据库与消息发布之间还有一致性缺口:可在同一数据库事务中保存业务变更和 outbox 记录,再由转发程序发布、等待 confirm、更新状态并重试;这与 broker 的持久化是不同层次的保障。
- 消费端:显式选择手动确认(Java
basicConsume的autoAck=false),业务事务成功提交后再basicAck。ACK 必须在收到 delivery tag 的同一信道发送。自动确认是在 broker 发送投递后视为完成,不是生产者发送完就清除消息;框架默认值需单独核对。 basicNack/basicReject的requeue=true请求重新入队,可能仍投给原消费者;requeue=false则进入已配置的死信流程,否则丢弃。连接或信道关闭后,未确认消息会重新入队,因此还必须保证幂等。
生产者 -- publish --> Exchange -- 路由 --> Queue -- delivery --> 消费者
生产者 <-- confirm -- Broker Queue <-- ACK -- 业务提交后这两种确认相互独立:生产者没有收到 confirm 时,消息可能已经入队;消费者业务提交后、ACK 前断线,消息也可能再次投递。可靠重试与幂等需要一起设计。
5、如何防止消息重复消费
消息链路是“生产者 → MQ → 消费者”,重复可能来自生产者重试,也可能来自消费后的重新投递。
- RabbitMQ 普通队列不会根据所谓
inner-msg-id自动去重,设置消息属性messageId本身也不会开启去重。生产者应为同一业务事件使用稳定的事件 ID,重试时沿用。 - 消费者以事件 ID 或合适的业务唯一键实现幂等。例如同一订单有多个合法事件,不能只用订单 ID 去重所有事件。
- 数据库场景可在同一事务内插入有唯一约束的消费记录并完成业务写入,提交后 ACK;重复键时确认该事件已经成功处理。不要先单独记录“已消费”再执行业务,否则中途失败会漏处理;调用外部支付等系统还需要对方支持幂等键。
官方 可靠性指南:重复投递与幂等 说明了确认丢失后重试可能产生重复,redelivered 只能作为重投提示,不能替代业务去重。
6、RabbitMQ的死信队列
死信交换机(DLX)是普通交换机;死信队列(DLQ)是绑定到它、接收死信的普通队列,两者不能混称。
消息成为死信的主要情况:
- 消费者
basicReject/basicNack且requeue=false; - 消息 TTL 到期;
- 超过队列长度限制而被移除,具体取决于 overflow 策略;
- quorum queue 的消息投递次数超过 delivery-limit。
整个队列因队列 TTL 到期被删除,不会把其中消息逐条死信化。配置时先声明 DLX 和 DLQ 并绑定,再在源队列上设置 dead-letter-exchange;还可设置 dead-letter-routing-key,未设置时沿用原路由键。声明参数写法是 arguments.put("x-dead-letter-exchange", "dlx.exchange"),但运维上通常优先使用可动态调整的 policy,避免硬编码参数后需要重建队列。
死信转发本身也有可靠性边界:默认转发不能保证目标队列不可用时不丢消息;quorum queue 可按条件配置 at-least-once dead-lettering,仍需处理重复。
参见官方 Dead Letter Exchanges。
7、RabbitMQ如何实现延迟消息的
- TTL + DLX:发送到没有消费者的等待队列,消息到期后经 DLX 路由到业务队列,业务消费者只监听后者。队列级消息 TTL(
x-message-ttl)单位为毫秒,不能误用删除空闲队列的x-expires。 - 同一队列混用不同的每条消息 TTL 时,短 TTL 消息可能被前面的长 TTL 消息挡住,直到到达队首才被死信化。因此它不是精确计时器;固定延迟可按延迟档位分队列,消费时仍需核对业务截止时间。
生产者 → 等待队列(无消费者,消息 TTL)
↓ 过期
DLX → 业务队列 → 消费者rabbitmq_delayed_message_exchange是需额外安装的插件,通过x-delayed-message交换机及x-delay消息头实现延迟路由,不能假设服务默认启用。插件发布版本需匹配 RabbitMQ 版本;其待投递消息的存储与故障恢复限制应按插件文档评估,不能直接套用 quorum queue 的复制保证。
参见官方 TTL 与过期 和 RabbitMQ 项目下的 延迟插件说明。
8、消息长时间堆积怎么处理
- 先区分
messages_ready与messages_unacknowledged:前者持续增加多为消费速率不足,后者很高可能是慢业务、ACK 遗漏或 prefetch 过大。检查异常日志、消费耗时与数据库瓶颈,再决定扩容。 - 修复消费者后,可为同一队列增加竞争消费者、适当调整 prefetch;并发上限受下游容量和业务顺序约束。RabbitMQ 的 topic 是交换机类型,没有 Kafka 那种“把 topic 的 partition 扩大十倍”的操作。若单队列成为瓶颈,可按业务键设计多队列分片,需同时调整路由与顺序保证。
- 过期消息若已配置 DLX,可从死信流程分析并补偿;已丢弃的消息不能从原队列找回,只能依赖业务数据库或 outbox 等可靠来源补发,并保留幂等控制。
- 队列长度限制默认移除队首消息,也可配置拒绝新发布的策略;磁盘或内存告警通常会阻塞发布连接,不能一概说成“放不下就丢弃后续消息”。应结合限流与可观测指标处理容量问题。
参见官方 Consumers、Queue Length Limit 与 资源告警。
9、RabbitMQ集群
- 普通集群模式
集群内共享队列、交换机、绑定等元数据,但消息是否复制取决于队列类型。4.1 的 classic queue 不复制,消息数据位于承载该队列的单个节点。客户端可以连接其他节点,由其转发请求到队列所在节点;这不会产生消息副本。
增加节点可承载更多分散的队列,但不会自动提升单个队列吞吐。单副本队列所在节点故障时,该队列会不可用,即使其他节点仍可提供服务,因此“组成集群”本身不等于队列高可用。
- 镜像队列模式
这是旧版 classic mirrored queues 的历史概念:按策略把指定队列复制到若干节点,并不是所有队列必然复制到所有节点。经典镜像队列从 RabbitMQ 4.0 起已移除,4.1 不应再按镜像队列方案部署。
需要复制与高可用时,可使用基于 Raft 的 quorum queue。它有一个 leader 和若干副本,需要多数成员可用才能继续服务;三个成员通常可容忍一个成员故障,失去多数时不能继续正常读写。声明时使用 durable 队列和 x-queue-type=quorum,不能把已有 classic queue 直接改类型,需要规划新队列迁移。复制也不能替代生产确认、消费确认与幂等。
参见官方 Queues 和 Quorum Queues。
