RabbitMQ 是什么
书面笔记版
RabbitMQ 是一个消息队列中间件,主要用于应用之间的异步通信、流量削峰、系统解耦和最终一致性处理。
它基于 AMQP 协议模型,生产者不直接把消息发给队列,而是先发给交换机,交换机再根据路由规则把消息投递到一个或多个队列,消费者从队列中拉取或接收消息并处理。
常见使用场景:
| 场景 | 说明 |
|---|---|
| 异步处理 | 下单后异步发送短信、邮件、站内信 |
| 流量削峰 | 秒杀请求先写入消息队列,消费者按能力慢慢处理 |
| 系统解耦 | 订单系统只发事件,不直接依赖库存、积分、通知系统 |
| 最终一致性 | 本地事务成功后发送消息,下游服务异步完成状态同步 |
| 延迟任务 | 订单超时关闭、支付超时取消、定时提醒 |
RabbitMQ 更偏传统业务消息中间件,特点是路由能力灵活、可靠性机制完整、使用简单,适合对业务解耦、可靠投递、延迟任务要求较多的场景。
面试口述版
RabbitMQ 是消息队列中间件,主要解决异步、削峰、解耦和最终一致性问题。它的核心模型是生产者把消息发到交换机,交换机根据路由规则投递到队列,消费者再从队列消费。相比 Kafka 这种日志型消息系统,RabbitMQ 更偏业务消息,路由灵活、可靠性机制比较完整,常用于订单、通知、延迟任务这类场景。
AMQP 核心模型
书面笔记版
RabbitMQ 的核心模型可以按生产者、交换机、队列、绑定、消费者来理解。
| 组件 | 作用 |
|---|---|
| Producer | 生产者,负责发送消息 |
| Exchange | 交换机,接收生产者消息并按规则路由 |
| Queue | 队列,真正存储消息 |
| Binding | 绑定关系,描述交换机和队列之间的路由规则 |
| Routing Key | 路由键,生产者发送消息时携带的路由标识 |
| Consumer | 消费者,从队列获取消息并处理 |
| Broker | RabbitMQ 服务节点 |
| Virtual Host | 虚拟主机,用于资源隔离 |
典型流程:
需要注意:消息不是直接发给消费者。队列负责存储消息,消费者只是从队列中消费。如果消费者暂时不可用,只要队列和消息满足持久化条件,消息可以继续保留在队列中等待消费。
面试口述版
RabbitMQ 里生产者通常不直接发队列,而是发给 Exchange。Exchange 根据 Routing Key 和 Binding 规则,把消息路由到对应 Queue,消费者再从 Queue 消费。Queue 才是真正存消息的地方,Exchange 只负责路由。Virtual Host 可以理解成逻辑隔离空间,不同业务可以放在不同 vhost 里。
交换机类型
书面笔记版
Exchange 决定消息如何从生产者路由到队列。常见交换机有 direct、topic、fanout、headers。
| 类型 | 路由规则 | 典型场景 |
|---|---|---|
| direct | Routing Key 完全匹配 Binding Key | 精确路由,如订单状态变更 |
| topic | 按通配符匹配 Routing Key | 多级主题路由,如 order.*、order.pay.# |
| fanout | 广播到绑定的所有队列,忽略 Routing Key | 广播通知、发布订阅 |
| headers | 按消息 header 匹配 | 较少使用,规则更复杂 |
topic 交换机常见通配符:
| 通配符 | 含义 |
|---|---|
* |
匹配一个单词 |
# |
匹配零个或多个单词 |
例如 order.pay.success:
order.*.success可以匹配。order.#可以匹配。pay.#不能匹配。
实际业务里 direct 和 topic 使用最多。direct 适合明确的业务事件,topic 适合按业务域、动作、状态做灵活订阅。
面试口述版
RabbitMQ 常见交换机有 direct、topic、fanout 和 headers。direct 是路由键完全匹配,适合精确路由;topic 支持 * 和 # 通配符,适合按主题订阅;fanout 是广播,绑定到这个交换机的队列都会收到消息;headers 按消息头匹配,用得比较少。实际项目里 direct 和 topic 最常见。
Routing Key 和 Binding Key 路由规则
书面笔记版
RabbitMQ 的 key 路由规则主要看两个概念:
| 概念 | 含义 |
|---|---|
| Routing Key | 生产者发送消息时携带的路由标识 |
| Binding Key | 队列绑定交换机时声明的匹配规则 |
消息发送时,生产者把消息发到 Exchange,并携带一个 Routing Key。队列通过 Binding 绑定到 Exchange 时,会设置 Binding Key。Exchange 根据自身类型,用 Routing Key 去匹配 Binding Key,匹配成功的队列就会收到消息。
不同交换机的 key 规则不同:
| 交换机类型 | key 路由规则 | 示例 |
|---|---|---|
| direct | Routing Key 必须和 Binding Key 完全一致 | order.pay.success 只匹配order.pay.success |
| topic | 按 . 分隔单词,支持 * 和 # 通配符 |
order.pay.success 可匹配order.*.success、order.# |
| fanout | 忽略 Routing Key,广播给所有绑定队列 | 只要队列绑定了该交换机就能收到 |
| headers | 忽略 Routing Key,按消息 header 匹配 | 根据 header 中的键值对匹配 |
topic 交换机的通配符要重点记:
| 通配符 | 含义 | 示例 |
|---|---|---|
* |
匹配一个单词 | order.*.success可匹配order.pay.success |
# |
匹配零个或多个单词 | order.#可匹配 order、order.pay、order.pay.success |
假设消息的 Routing Key 是 order.pay.success:
| Binding Key | 是否匹配 | 原因 |
|---|---|---|
order.pay.success |
匹配 | direct 或 topic 下都可以完全匹配 |
order.*.success |
匹配 | * 匹配中间的 pay 一个单词 |
order.# |
匹配 | # 匹配后面的多个单词 |
order.* |
不匹配 | 只匹配两个单词,消息有三个单词 |
pay.# |
不匹配 | 第一个单词就不一致 |
还要注意几个细节:
- 一个 Exchange 可以绑定多个 Queue。
- 一个 Queue 可以用多个 Binding Key 绑定到同一个 Exchange。
- 如果多个 Queue 的 Binding Key 都匹配同一条消息,这些 Queue 都会收到消息。
- 如果消息没有路由到任何队列,默认会被丢弃;如果生产者开启
mandatory,可以通过 Return Callback 感知不可路由消息。 - 默认交换机是一个特殊的 direct exchange,可以用队列名作为 Routing Key,把消息直接路由到同名队列。
面试口述版
RabbitMQ 的 key 路由要区分 Routing Key 和 Binding Key。Routing Key 是生产者发消息时带的 key,Binding Key 是队列绑定交换机时配置的匹配规则。direct 要完全匹配,topic 支持 * 和 # 通配符,fanout 直接忽略 key 做广播,headers 按消息头匹配。比如 order.pay.success 可以匹配 order.*.success 和 order.#。如果多个队列都匹配,消息会投递到多个队列;如果一个队列都匹配不到,默认会丢弃,开启 mandatory后生产者可以收到不可路由回调。
消息可靠性怎么保证
书面笔记版
RabbitMQ 的可靠性要分三段看:生产者到 Broker、Broker 自身存储、Broker 到消费者。
| 阶段 | 风险 | 解决方式 |
|---|---|---|
| 生产者到 Broker | 消息没发到交换机或队列 | Publisher Confirm、Return Callback、失败重试 |
| Broker 存储 | Broker 重启后消息丢失 | 交换机持久化、队列持久化、消息持久化 |
| Broker 到消费者 | 消费者处理失败但消息被删除 | 手动 ACK、NACK、重试、死信队列 |
生产端可靠性:
- 开启 Publisher Confirm,Broker 收到消息后回调确认。
- 开启 Return Callback,消息无法路由到队列时通知生产者。
- 发送失败可以重试,但要控制次数,避免无限重试。
- 核心业务可以配合本地消息表,先把待发送消息落库,再异步发送和补偿。
Broker 端可靠性:
- exchange 声明为 durable。
- queue 声明为 durable。
- message 设置 delivery mode 为 persistent。
- 如果对可用性要求更高,可以使用镜像队列或 quorum queue。
消费端可靠性:
- 使用手动 ACK,业务处理成功后再确认。
- 业务失败时根据错误类型选择重试、丢弃或进入死信队列。
- 消费者必须保证幂等,因为网络抖动、重试、消费者宕机都可能导致重复投递。
可靠性不是只开一个参数就完成的。生产确认、持久化、手动 ACK、幂等消费和补偿机制要配合使用。
面试口述版
RabbitMQ 可靠性我会分三段说。生产者到 Broker 用 Confirm 确认消息有没有到达 Broker,用 Return Callback 处理不可路由消息;Broker 自身要保证 exchange、queue 和 message 都持久化;消费者侧用手动 ACK,业务处理成功后再 ack,失败时重试或进死信队列。还要强调消费者必须幂等,因为消息中间件通常只能保证至少一次投递,不能保证绝对只投递一次。
ACK、重试和死信队列
书面笔记版
ACK 是消费者告诉 RabbitMQ 消息处理结果的机制。常见确认方式:
| 方式 | 含义 |
|---|---|
basicAck |
消费成功,RabbitMQ 删除消息 |
basicNack |
消费失败,可选择重新入队或丢弃 |
basicReject |
拒绝单条消息,可选择重新入队或丢弃 |
自动 ACK 风险较高,因为消费者一拿到消息,RabbitMQ 就认为消息已经成功消费。如果消费者处理过程中宕机,消息会丢失。核心业务一般使用手动 ACK。
死信队列本质是普通队列,只是绑定到了死信交换机。消息成为死信后,会被 RabbitMQ 投递到配置的死信交换机,再路由到死信队列。
常见死信原因:
| 原因 | 说明 |
|---|---|
| 消息被拒绝 | basicReject 或basicNack,且不重新入队 |
| 消息过期 | 消息 TTL 或队列 TTL 到期 |
| 队列满了 | 队列达到最大长度,旧消息或新消息被挤出 |
重试设计要避免无限循环。常见做法是:
- 可恢复异常短暂重试。
- 达到最大重试次数后进入死信队列。
- 死信队列由人工、定时任务或补偿程序处理。
- 消费失败日志要包含 messageId、业务 key、异常原因,方便排查。
面试口述版
ACK 是消费者告诉 RabbitMQ 消息有没有处理成功。核心业务一般用手动 ACK,业务处理成功后 basicAck;失败时可以 basicNack或 basicReject,选择重新入队或丢弃。如果消息被拒绝且不重新入队、消息过期、队列满了,就可能进入死信队列。重试一定要限制次数,否则可能因为一条坏消息一直循环消费,把系统拖垮。
如何保证消息不重复消费
书面笔记版
RabbitMQ 无法从根上保证消息绝对不重复。它更常见的语义是至少一次投递:消息尽量不丢,但可能重复。
重复消费的常见原因:
- 消费者处理成功后,ACK 发送失败。
- 消费者处理过程中超时或宕机,消息被重新投递。
- 生产者发送失败后重试,实际 Broker 已经收到第一条消息。
- 业务补偿或人工重发消息。
解决重复消费的核心是业务幂等。
| 方案 | 适用场景 |
|---|---|
| 唯一索引 | 创建订单、创建支付单等不能重复插入的场景 |
| 消费记录表 | 用 messageId 或业务唯一键记录消费状态 |
| 状态机判断 | 订单状态、支付状态、退款状态流转 |
| Redis 去重 | 高并发短周期去重,配合过期时间 |
| 乐观锁 | 更新类操作,按版本号或状态条件更新 |
幂等 key 优先使用业务唯一键,比如订单号、支付流水号、退款单号。单纯使用 RabbitMQ 的消息投递标识不一定能覆盖业务重发、补偿重发等场景。
面试口述版
RabbitMQ 不能假设消息只消费一次,实际要按至少一次投递来设计。重复消费可能是消费者业务成功但 ACK 失败,也可能是生产端重试或补偿重发。解决方式不是依赖 MQ,而是业务幂等。比如订单表加唯一索引,消费记录表记录 messageId 或业务唯一号,订单支付类用状态机判断,必要时 Redis 做短期去重。
如何保证顺序消费
书面笔记版
RabbitMQ 的顺序消费要先明确范围:通常只能保证同一个队列内、单个消费者串行处理时的局部顺序,不能天然保证全局顺序。
顺序被打乱的常见原因:
- 多个队列并行消费。
- 一个队列被多个消费者并发消费。
- 消费失败后重试,后面的消息先被处理。
- 业务处理使用异步线程池,完成顺序和拉取顺序不一致。
常见方案:
| 目标 | 做法 | 代价 |
|---|---|---|
| 单队列严格顺序 | 一个队列只绑定一个消费者串行处理 | 吞吐低 |
| 按业务 key 有序 | 相同业务 key 路由到同一个队列 | 只能保证局部顺序 |
| 失败不乱序 | 失败消息暂停后续处理或转入单独补偿流程 | 实现复杂 |
实际项目中更推荐缩小顺序范围,比如只要求同一个订单的状态消息有序,而不是所有订单消息全局有序。可以按订单 ID 哈希路由到固定队列,队列内部单消费者处理。
面试口述版
RabbitMQ 一般不谈全局顺序,更多是保证某个业务维度的局部顺序。比如同一个订单的消息要有序,可以把相同订单 ID 路由到同一个队列,并且这个队列用单消费者串行处理。只要多队列、多消费者并发、失败重试或异步线程池处理,都可能打乱顺序。严格顺序的代价是吞吐下降,所以要先明确顺序范围。
延迟消息怎么实现
书面笔记版
RabbitMQ 本身的常规队列不是天然延迟队列,常见实现方式有两类:TTL + 死信队列、延迟消息插件。
TTL + 死信队列流程:
核心思路:
- 生产者把消息发送到一个不被业务消费者直接消费的延迟队列。
- 延迟队列设置 TTL,并配置死信交换机。
- 消息过期后变成死信,被投递到死信交换机。
- 死信交换机把消息路由到真正的业务队列。
TTL 可以设置在队列上,也可以设置在消息上。
| 方式 | 特点 |
|---|---|
| 队列 TTL | 同一个队列里的消息延迟时间一致,简单稳定 |
| 消息 TTL | 每条消息可以有不同过期时间,但可能受队头阻塞影响 |
| 延迟消息插件 | 支持更自然的延迟投递,使用前要确认插件可用性 |
TTL + 死信队列适合固定延迟场景,比如 15 分钟未支付关闭订单。如果需要大量不同延迟时间,或者对延迟精度要求较高,要评估插件、时间轮或专门的任务调度系统。
面试口述版
RabbitMQ 做延迟消息常见有两种方式。第一种是 TTL 加死信队列:消息先进入延迟队列,不直接消费,TTL 到期后变成死信,再通过死信交换机进入真正的业务队列。第二种是使用延迟消息插件。固定延迟场景用 TTL 加死信比较常见,但如果每条消息延迟时间差异很大,要注意队头阻塞和延迟精度问题。
消息堆积怎么处理
书面笔记版
消息堆积说明生产速度长期大于消费速度,或者消费者出现故障。处理时要先定位原因,再扩容或限流。
常见原因:
- 消费者宕机或连接异常。
- 消费逻辑变慢,比如下游数据库、第三方接口超时。
- 单条消息处理太重,消费线程数不足。
- 预取值设置不合理,部分消费者拿了很多消息但处理不过来。
- 突发流量超过消费者处理能力。
处理思路:
| 方向 | 做法 |
|---|---|
| 快速止血 | 恢复消费者、降级慢接口、暂停非核心生产者 |
| 提升消费能力 | 增加消费者实例、增加队列分片、优化业务处理 |
| 控制入口流量 | 限流、削峰、生产端降级 |
| 调整消费参数 | 合理设置 prefetch,避免单个消费者囤积过多未确认消息 |
| 保护下游 | 消费端加超时、熔断、批处理和连接池保护 |
如果单个队列已经成为瓶颈,单纯增加消费者不一定有效。可以按业务 key 做多队列分片,让不同消费者组并行处理不同分片。
堆积处理后还要补监控:
- 队列 ready 消息数量。
- unacked 消息数量。
- 消费速率和生产速率。
- 消费失败率和重试次数。
- 消费延迟。
面试口述版
消息堆积本质是生产速度大于消费速度,或者消费者故障了。我会先看消费者是否存活、下游接口和数据库是否变慢,再看队列 ready 和 unacked 数量。处理上可以恢复消费者、扩容消费实例、优化消费逻辑、调整 prefetch、对生产端限流。如果单队列成为瓶颈,还要做多队列分片。最后要补监控,盯生产速率、消费速率、堆积量和失败率。
RabbitMQ 和 Kafka、RocketMQ 怎么选
书面笔记版
选型要结合业务场景,不是简单说谁更好。
| 中间件 | 特点 | 更适合 |
|---|---|---|
| RabbitMQ | 路由灵活、可靠性机制完整、业务消息友好 | 业务解耦、异步通知、延迟任务、传统企业应用 |
| Kafka | 高吞吐、分区日志、适合流式处理 | 日志采集、埋点、实时计算、大数据链路 |
| RocketMQ | 业务消息能力强,事务消息、延迟消息等支持较好 | 订单交易、金融电商、分布式事务最终一致 |
RabbitMQ 的优势:
- Exchange 路由模型灵活。
- ACK、Confirm、死信队列等机制成熟。
- 上手成本低,适合中小型业务系统。
RabbitMQ 的限制:
- 超高吞吐和海量日志场景通常不如 Kafka。
- 全局顺序、长时间海量消息堆积不是它的优势场景。
- 集群、镜像队列、quorum queue 的运维复杂度需要评估。
面试中可以这样归纳:业务消息、复杂路由、异步解耦可以选 RabbitMQ;日志流、大吞吐、流处理优先 Kafka;电商交易、事务消息、顺序消息和延迟消息能力要求较强时可以考虑 RocketMQ。
面试口述版
RabbitMQ、Kafka、RocketMQ 主要看场景。RabbitMQ 路由灵活,可靠性机制成熟,适合业务解耦、异步通知、延迟任务。Kafka 是高吞吐分布式日志,更适合日志、埋点、实时计算。RocketMQ 更偏业务消息,在事务消息、延迟消息、顺序消息这些场景里比较常见。面试里不要简单说谁更好,要结合吞吐、可靠性、顺序、延迟和运维成本来选。
高频追问速答
书面笔记版
| 问题 | 回答要点 |
|---|---|
| 消息丢失怎么办 | 生产 Confirm、Return Callback、持久化、手动 ACK、本地消息表和补偿 |
| 消息重复怎么办 | 接受至少一次投递语义,业务侧用唯一索引、消费表、状态机做幂等 |
| 消息积压怎么办 | 查消费者和下游瓶颈,扩容消费者,调 prefetch,必要时队列分片和生产限流 |
| 消息顺序怎么保证 | 相同业务 key 路由到同队列,单消费者串行处理,牺牲吞吐 |
| 死信队列有什么用 | 接收失败、过期、队列满导致的死信,便于补偿、排查和延迟消息 |
| 延迟消息怎么做 | TTL + 死信队列,或使用延迟消息插件 |
| 为什么消费者要手动 ACK | 避免消息刚投递给消费者就被删除,业务失败或宕机时消息丢失 |
| prefetch 是什么 | 限制消费者一次最多拿多少未确认消息,避免单个消费者囤积过多消息 |
RabbitMQ 相关面试题通常不是考某个 API,而是考消息可靠性、幂等、堆积、顺序和延迟任务这些工程问题。回答时最好按链路拆开:生产端、Broker、消费端、业务补偿。
面试口述版
RabbitMQ 高频问题可以围绕可靠性和消费问题来答。消息丢失就按生产确认、持久化、手动 ACK、补偿机制说;消息重复就强调至少一次投递和业务幂等;消息堆积就查消费者、下游瓶颈、prefetch、扩容和限流;顺序消费要缩小到业务 key 维度;延迟消息常用 TTL 加死信队列或延迟插件。核心思路是不要只背 API,要按生产端、Broker、消费端和业务兜底完整说明。