RabbitMQ 面试问答

1. 为什么要消息队列?

很多业务不是一个接口干到底,下单之后还要发短信、推仓储、写搜索索引------如果全同步串行,一个环节慢或挂了,整条链路都受影响。

消息队列解决三类问题:

  • 异步:主流程只负责发消息,下游慢慢处理,用户不用干等
  • 解耦:订单服务不用直接依赖短信、仓储,各改各的,接口不变
  • 削峰:秒杀瞬间流量先堆在队列里,消费者按自己的处理能力慢慢消费

代价是系统变复杂:重复消费、顺序、延迟、运维都要额外考虑。MQ 换的是可用性和扩展性,不是加了就更快。


2. 什么场景用 RabbitMQ?

适合:服务之间发业务事件、任务分发、需要灵活路由(不同消息进不同队列)、吞吐量在万级/秒量级。

不太适合:调用方必须立刻拿到下游结果才能继续;要回放全量历史日志做流式计算(更适合 Kafka);团队没人运维却上了要精细调参的集群。

和 Redis 队列比:Redis List / Stream 适合量小、能丢几条;RabbitMQ 有确认、持久化、死信,适合业务消息要可靠投递的场景。


3. 消息怎么从生产者到消费者?

RabbitMQ 走 AMQP 模型,路径是:

text 复制代码
生产者 -> Exchange(交换机)-> Queue(队列)-> 消费者

Exchange 不存消息 ,只负责路由------看 routing key 和 binding 规则,决定消息进哪个 Queue。Queue 才存消息,等消费者来取。

很多人误以为生产者直接发 Queue,其实中间必须经过 Exchange(除非用默认 Exchange,routing key 必须等于 queue 名,那是 direct 的特例)。

Binding 是 Queue 和 Exchange 之间的绑定关系,声明什么样的 routing key 能进我这个队列。


4. Exchange 四种类型

类型 怎么路由 典型用法
direct routing key 和 binding key 完全相等 按业务类型分发:支付走支付队列,取消走取消队列
fanout 不看 routing key,广播到所有绑定的队列 配置刷新、缓存失效,通知所有节点
topic routing key 按 . 分段,* 匹配一个词,# 匹配零个或多个 日志分级:order.* 收所有订单日志,order.payment.# 只收支付相关
headers 按消息头匹配 几乎不用,知道有就行

日常 direct 和 topic 最多。fanout 做广播。


5. 怎么保证消息不丢?

可靠性要保三段,缺一段都不行:

① 生产者 → Broker

发完不能默认成功。开 Publisher Confirm,Broker 收到并路由后会回 ack;没有队列能接收时回 nack 或 return(需开 mandatory)。别用事务模式,性能差。

② Broker 存盘

Broker 重启消息还在,需要三个开关同时开:

  • Exchange 声明 durable = true
  • Queue 声明 durable = true
  • 消息 delivery mode = 2(persistent)

持久化是写磁盘,不是魔法,仍受刷盘策略影响,且会降低吞吐。

③ Broker → 消费者

生产环境用 manual ack ,业务处理成功再确认。auto ack 是一推给消费者就确认了,处理到一半进程挂了,消息就丢了。

  • Ack:处理成功,告诉 Broker 这条可以删了
  • Nack (Negative Acknowledgment):处理失败,告诉 Broker 这条没消费成功
    • requeue: true:放回队列,稍后重试
    • requeue: false:不再重投,配合死信交换机进 DLQ

6. Prefetch 是什么?

basicQos(prefetchCount: n) 限制单个 Channel 上还没 ack 的消息最多几条

不是把总数平均分给所有消费者。每个消费者各自有一个 Channel,各自有自己的 prefetch 上限。Broker 推消息时,某消费者 unacked 条数已达上限,就不再给它推,转给其他还没满的消费者。

prefetch = 1 是常见配置:每个消费者同一时间最多拿 1 条,ack 后才拿下一条。多消费者共抢一个队列时,快慢消费者之间分配较均匀,慢消费者也不会一口气占很多消息不 ack。代价是吞吐略低(往返次数多);消息小、处理快时可调到 10~50。

不设 prefetch 时,Broker 可能一次性推几百条给某个消费者,慢消费者内存压力大,其他消费者反而空闲。prefetch 是 per-channel 的,每个消费线程用自己的 Channel,各自配置。


7. 重复消费与投递语义

第 5 节讲的是消息不丢。这一节讲另一个问题:同一条消息,消费者可能收到不止一次。

语义 含义 RabbitMQ 能不能做到
最多一次 可能丢,但不会重复 auto ack 就是这样,不推荐
至少一次 不会丢,但可能重复 Confirm + 持久化 + manual ack,生产常见
恰好一次 不丢也不重复 Broker 端到端做不到,靠业务幂等兜底

RabbitMQ 配好了可靠性,实际就是至少一次:消息一定能送到,但同一条可能被消费两次甚至更多。MQ 不负责去重,消费端要自己扛。

重复常见来源有三类。生产者重试 :发完没收到 Confirm,以为失败又发一遍,队列里两条内容一样的消息。消费者 ack 超时或忘了 ack :业务已跑完(DB 已扣库存),但 Ack 没发出去或处理超时,Broker 认为还没消费成功,会重新投递。网络抖动:Ack 包丢了,Broker 没收到确认,同样重投。本质都是 Broker 不知道处理完了,所以再发一次。

幂等 指同一个操作执行多次,结果和执行一次一样。支付消息来两次,库存只能扣一次。消费端不能假设消息只来一次,重复只能在消费端挡:用 orderId + eventType 做幂等表,插过就跳过;或对业务键加数据库唯一约束;或用状态机,订单已是 Paid 就不再扣款。


8. 死信队列(DLX)

正常队列里的消息,消费者取出来处理、Ack 掉。有些消息在正常流程里走不下去了------消费不了、过期了、队列满了------既不能一直占着业务队列重试,也不能直接丢掉,这类叫死信(Dead Letter)

**死信队列(DLQ)**专门收这些消息,相当于业务队列旁边的隔离区。配置上:业务队列声明 x-dead-letter-exchange 指向死信交换机(DLX),DLX 再绑定 DLQ;消息一旦变成死信,Broker 自动转过去。

变成死信的情况:Nack(requeue: false) 不再重试;消息或队列 TTL 过期;队列超过 max-length 被挤出。

DLQ 三个常见用途。隔离毒消息 :每次消费都抛异常的消息,若一直 Nack(requeue: true) 会无限循环;本地重试 N 次仍失败就 Nack(requeue: false) 进 DLQ,业务队列恢复。人工排查补偿 :查看 DLQ 内容、修数据后重新发回或脚本补偿。延迟投递:消息设 TTL,过期变死信进 DLQ,消费者监听 DLQ 相当于延迟到了才处理(见第 10 节)。

DLQ 和业务队列分开建,用于告警、排查、补偿,不混在主流程里。


9. 消息顺序能保证吗?

单队列 + 单消费者:基本有序。

同一队列多个消费者:并发处理,顺序没法保证。

要大体有序:按业务键 hash 到固定队列(比如 orderId % 4 进 4 个队列之一),每个队列只开一个消费者;或者业务层容忍乱序,用版本号做合并。别说 MQ 保证顺序。


10. 延迟消息

订单 30 分钟未支付自动关单,需要延迟投递。常见做法:

  • TTL + DLX:消息设 TTL,过期后进死信队列,相当于延迟到了才消费
  • delayed-message-exchange 插件:最直接
  • 业务层延迟表 + 定时扫描:不依赖 MQ 特性

TTL + DLX 要注意:同队列里不同 TTL 的消息,队头没过期会堵住后面的,叫队头阻塞。


11. 和 Kafka 怎么选?

RabbitMQ Kafka
定位 消息代理,智能路由 分布式日志,高吞吐流
消费后 通常 ack 后从队列删除 消息保留,可回溯重放
吞吐量 万级/秒 百万级/秒
典型场景 任务分发、业务事件、服务解耦 日志采集、流计算、事件溯源

订单支付通知仓储、发短信------RabbitMQ 合适。用户行为日志进数仓、Flink 实时聚合------Kafka 合适。不是谁更好,是问题类型不同。


12. 本地事务和发 MQ 怎么一致?

用户下单,同一个操作里既要写数据库(插订单),又要发 MQ 通知仓储备货。至少不能出现:库里有订单但仓储没收到消息,或仓储收到消息但订单没建成。

数据库和 RabbitMQ 是两个独立系统,先写 DB 再发 MQ 是两步,任何一步都可能单独失败。DB 成功、发 MQ 前程序崩溃------仓储永远不知道。MQ 发出去了、DB 事务回滚------仓储收到备货通知,订单不存在。在 DB 事务里直接 BasicPublish 也解决不了,MQ 提交不归 DB 管,DB 回滚带不走已发出的消息。

Outbox 的做法:发 MQ 这件事先记到 DB 里,跟业务数据进同一个本地事务。

text 复制代码
1. 开启 DB 事务,插入订单表,插入 outbox 表(payload、status=pending)
2. 提交事务 ------ 订单和 outbox 要么都在,要么都不在
3. 后台 Worker 扫 outbox,发到 RabbitMQ,成功则标记 sent

Worker 发完还没标记就挂了,可能重复发,靠消费者幂等(第 7 节)。还没发就挂了,outbox 仍是 pending,下次重试。最终至少会发出去一次。

消费端同理:DB 事务处理业务并提交,成功再 Ack,失败 Nack;Ack 前做好幂等。DB 和 MQ 的一致性靠 Outbox + 消费者幂等做最终一致。


13. 消息堆积怎么排查?

先看队列深度,分 ready (等着被消费)和 unacked(已推给消费者还没 ack):

  • unacked 高:消费者慢、卡住了、忘了 ack、prefetch 设太大
  • ready 高、消费者少:扩容消费者,或优化单条处理逻辑
  • 没有消费者:订阅丢了、连接断了、vhost 权限不对
  • 单条处理太慢:瓶颈在下游 DB 或 API,加消费者没用

先分清是生产太快还是消费太慢,再动刀。内存/磁盘告警时 RabbitMQ 会流控阻塞生产者,常见原因也是消费者停了或消息不 ack 堆着。


14. 高可用与集群

RabbitMQ 的高可用分两层:多台 Broker 组集群 ,以及队列多副本(消息不随单节点挂掉而丢)。

集群是多台节点连成一组,共享 Exchange、Queue、Binding 等元数据定义。客户端连任意节点都能发收消息,节点之间同步路由、互相转发。三台组了集群,不等于三条消息分别存三台------集群同步的是定义,普通队列里的消息内容默认只放在声明这个队列的那台节点上。

集群有 A、B、C 三台,应用在 A 上声明了 order.queue

text 复制代码
A 节点:存着 order.queue 里的 1000 条消息
B、C 节点:知道 order.queue 存在,可转发,但本地没有这 1000 条

连 B 发消息,B 转给 A 存;连 B 消费,B 从 A 取。A 挂了,这个普通队列暂时不可用。集群管的是组网、路由、接入:拓扑全集群一致,连接可分散到任意节点,Quorum Queue 等多副本方案也得以集群为前提。消息本身要不丢,还得靠下面的队列类型。

类型 说明
普通队列 数据在单个节点,节点挂了就完了
经典镜像队列 老方案,主从同步复制,有脑裂等问题,老项目可能还在用
Quorum Queue 3.8+ 推荐,Raft 共识多副本,主节点挂了自动切换

生产新部署优先 Quorum Queue,集群至少 3 节点,副本数通常 3。

和 Redis Cluster 不同:Redis 按 key 分片,每个 key 只在一台;RabbitMQ 集群是组网 + 队列选副本策略,一条队列可通过 Quorum 在多个节点有副本。

RabbitMQ 不是消息追踪系统,消息 ack 后从队列删除,没法按 ID 查历史;追踪靠业务层日志或 outbox 状态表。

相关推荐
Hyyy2 小时前
流式渲染的基础——SSE
前端·后端·面试
hkNaruto2 小时前
【大数据】《传统Hadoop大数据技术入门:补充与进阶——从数据格式到智能决策的问答实录》
大数据·hadoop·分布式
用户852495071842 小时前
手把手教你打造专属 AI 技能(SKILL):从会议纪要秘书到 AI 日报主编
面试
KaifuZeng3 小时前
电路面试问题汇总一
单片机·嵌入式硬件·面试·电路
ThanksGive3 小时前
TurboLock:从 v1 Redis 分布式锁到 v2 Go 并发控制中间件
分布式·后端
不要葱花3 小时前
接下来我将复现 10 篇强化学习算法:第 3 篇,一杯喜茶,搞定 Search-R1
算法·面试
嘟嘟07173 小时前
像水流一样——前端流式输出完整指南(3)
面试·github
weedsfly3 小时前
从表单校验到订单流转,彻底搞懂前端开发中的策略模式与状态模式
前端·javascript·面试
帅次4 小时前
Android 高级工程师面试:Flutter 路由导航 近1年高频追问 18 题
android·flutter·面试