技术派面试拷打①:RabbitMQ 异步解耦与消息可靠性
技术派面试拷打①:RabbitMQ 异步解耦与消息可靠性
简历原文:「将点赞通知等事件通过 RabbitMQ 异步解耦,消费端手动 Ack 保障消息至少处理一次;针对消息可靠性设计了生产端 Confirm 与死信兜底方案。」
面试口径:“点赞通知用 RabbitMQ 做异步解耦:用户点赞立即返回,通知消息异步处理,核心链路和非核心链路解耦。消费端用 manual ack,业务处理完成后才 basicAck,保证 at-least-once;重复消费靠 notify 表唯一索引做幂等。生产端开启 Confirm 回调,Broker 持久化失败后 nack 触发重发;消费失败进入死信队列(DLQ)人工兜底。”
第一层:为什么需要消息队列?(架构设计题)
Q1:你们项目里 RabbitMQ 是必须的吗?不用行不行?
答: 不是必须的,但用了之后架构更清晰。
不用 MQ 的问题(同步通知):
1 | |
这一套下来,用户要等 所有操作都完成 才能看到”点赞成功”,如果通知生成很慢,用户就卡住了。
用了 MQ 之后(异步通知):
1 | |
用户完全不用等通知生成,核心链路(点赞)和辅助链路(通知)解耦了。
项目里的实际代码(UserFootServiceImpl.java):
1 | |
不用 MQ 会怎样? 如果通知生成要 200ms,用户点赞后就要等 200ms 才能继续操作。用 MQ 后,用户 5ms 内就能继续刷下一篇文章。
Q2:RabbitMQ、Kafka、RocketMQ 怎么选?(字节必考)
答: 三个的定位不一样,选型要看场景。
| 维度 | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|
| 定位 | 传统消息队列,功能丰富 | 分布式流平台,吞吐量极高 | 阿里开源,电商场景打磨 |
| 吞吐量 | 万级 ~ 十万级 QPS | 百万级 QPS | 十万级 ~ 百万级 QPS |
| 消息可靠性 | 支持事务、手动 Ack,很可靠 | 靠副本机制,可能丢消息 | 支持事务消息、顺序消息 |
| 延迟 | 微秒 ~ 毫秒级(最低) | 毫秒级 | 毫秒级 |
| 顺序消息 | 不支持 | 分区内有序 | 支持(电商订单场景) |
| 死信队列 | 原生支持(DLX) | 不支持(要自己实现) | 支持 |
| 延迟消息 | 需装插件(rabbitmq-delayed-message-exchange) | 不支持 | 原生支持(很方便) |
本项目选 RabbitMQ 的理由:
- 消息量不大(论坛点赞/评论频率),不需要 Kafka 的百万级吞吐
- 需要消息可靠性(通知消息不能丢),RabbitMQ 的手动 Ack + Confirm 机制很成熟
- 需要死信队列兜底,RabbitMQ 原生支持,配置简单
- 运维简单,RabbitMQ Management UI 很好用,适合个人项目
面试回答模板:
“日志采集/大数据管道用 Kafka,电商订单/事务消息用 RocketMQ,业务解耦/通知类用 RabbitMQ。我们项目是论坛场景,消息量不大但要求可靠性,所以选了 RabbitMQ。”
第二层:生产端消息可靠性
Q3:你说生产端用了 Confirm 机制,具体是怎么工作的?
答: Confirm 机制是 RabbitMQ 的生产端确认机制,保证消息成功到达 Broker。
不用 Confirm 的问题:
1 | |
消息在网络传输中丢了,生产者完全不知道。
Confirm 机制的工作原理:
- 开启 Confirm:
channel.confirmSelect() - 发送消息:正常发送,RabbitMQ 收到后会异步回调
- ack 回调:RabbitMQ 成功持久化后,调用
ConfirmListener的handleAck(告诉生产者:消息安全了) - nack 回调:RabbitMQ 内部错误(比如磁盘满了),调用
handleNack(告诉生产者:消息丢了,重发吧)
项目里的实际代码(RabbitmqServiceImpl.java):
1 | |
waitForConfirmsOrDie(3000) 是什么意思?
- 同步等待 Broker 的 Confirm 回调,最多等 3 秒
- 如果 3 秒内没收到 ack → 抛异常,捕获后记录日志/重发
- 如果收到 nack → 直接抛异常
Confirm 和事务(tx)的区别?
| Confirm(推荐) | 事务(tx) | |
|---|---|---|
| 性能 | 高(异步回调) | 低(同步阻塞,性能降 250 倍) |
| 可靠性 | 同样可靠 | 同样可靠 |
| 用法 | confirmSelect() + 监听回调 |
txSelect() + txCommit() |
面试必说: “Confirm 是异步确认,性能远高于事务消息,生产环境都用 Confirm。”
Q4:Confirm 机制是异步的,项目里为什么还要用 waitForConfirmsOrDie 同步等待?
答: 这是一个取舍问题,项目里用的是”同步等待 Confirm”,适合消息量不大的场景。
纯异步 Confirm 的问题:
1 | |
如果消息量很大,纯异步更好(不阻塞发送线程)。但如果消息量不大,用 waitForConfirmsOrDie 同步等待更简单:
- 发送 → 阻塞等待 Broker 确认 → 确认成功才继续
- 如果超时或 nack → 立即知道,可以重发
更好的方案(生产环境):
1 | |
面试回答:
“我们项目里消息量不大,用
waitForConfirmsOrDie同步等待比较简单。如果消息量很大,应该用纯异步 Confirm + 失败重试队列,避免阻塞发送线程。”
Q5:如果 RabbitMQ 宕机了,生产端发送的消息会丢失吗?
答: 会丢失,除非你做了消息持久化。
RabbitMQ 宕机消息丢失的三个阶段:
阶段一:消息在 JVM 内存里,还没发给 RabbitMQ
- 解决:消息先存本地 DB(状态=待发送),发送成功再改状态
阶段二:消息已发给 RabbitMQ,但 RabbitMQ 还没持久化(在内存里)就宕机了
- 解决:① 开启 Confirm 机制,确保 Broker 持久化后才算成功;② 队列和消息都设置持久化
阶段三:RabbitMQ 持久化了,但磁盘坏了
- 解决:RabbitMQ 镜像队列(高可用),消息存多台机器
项目里的持久化配置(RabbitmqServiceImpl.java):
1 | |
面试加分: “我们项目里 Exchange 和 Queue 都设置了持久化,但消息本身的 deliveryMode 没显式设置,这是个可以优化的点,应该设置为 MessageProperties.PERSISTENT_TEXT_PLAIN(即 deliveryMode=2)。”
第三层:消费端手动 Ack 与消息可靠性
Q6:消费端手动 Ack 是怎么工作的?为什么不用自动 Ack?
答: 自动 Ack 是”发给消费者就立即确认”,如果消费者拿到消息后处理到一半宕机了,这条消息就永久丢失了。
自动 Ack 的问题:
1 | |
手动 Ack 的工作原理:
1 | |
项目里的实际代码(RabbitmqServiceImpl.java):
1 | |
basicAck 的第二个参数 multiple 是什么意思?
false:只确认当前这条消息(deliveryTag)true:确认当前这条 + 所有比它 deliveryTag 小的未确认消息(批量确认,提升性能)
面试回答:
“自动 Ack 是发出去就确认,消费者拿到消息后宕机了消息就丢了。手动 Ack 是等业务处理完成后才确认,如果处理中途宕机,RabbitMQ 会把消息重新分发给其他消费者,保证 at-least-once。”
Q7:手动 Ack 保证了 at-least-once,那重复消费怎么办?你们项目里怎么处理幂等性?
答: at-least-once 必然导致重复消费,要解决幂等性问题。
重复消费的场景:
1 | |
幂等性设计:同样的消息,消费 1 次和消费 10 次,效果一样。
方案 1:数据库唯一索引(最常用)
1 | |
方案 2:Redis SetNX(分布式锁)
1 | |
项目里的幂等处理: 从代码看,项目用 notifyService.saveArticleNotify() 保存通知,应该在 notify 表有唯一索引,防止重复插入。
面试回答:
“我们项目里用 notify 表的唯一索引(user_id + article_id + type)做幂等,重复消费时数据库报 DuplicateKeyException,捕获后直接 Ack 跳过。也可以用 Redis SetNX 做幂等,但数据库唯一索引更简单可靠。”
Q8:如果消费者处理消息时一直卡住(比如远程调用超时),会怎么样?
答: RabbitMQ 的 消费超时机制 会触发,但如果没设置,消息会一直卡在 Unacked 状态。
问题场景:
1 | |
解决方案:
1. 设置 basicQos(限制每个消费者同时处理的最大消息数)
1 | |
2. 设置消费超时(Spring AMQP 的 @RabbitListener)
1 | |
3. 业务代码里设置超时
1 | |
面试回答:
“如果消费者一直卡住,消息会一直处于 Unacked 状态,RabbitMQ 不会重新分发。解决方法是:①
basicQos限制每个消费者同时处理的最大消息数;② 业务代码里设置远程调用超时;③ 超时时用basicNack(requeue=false)让消息进入死信队列,不让它一直卡住。”
第四层:死信队列(DLQ)兜底方案
Q9:死信队列(Dead Letter Queue)是什么?你们项目里怎么用的?
答: 死信队列是装”无法正常消费的消息”的队列,相当于”垃圾回收站”。
消息变成”死信”的三种情况:
- 消费者拒绝消息(
basicReject/basicNack)且requeue=false(不重新入队) - 消息 TTL 过期(设置了消息的存活时间,超时没人消费)
- 队列达到最大长度(队列满了,最早进来的消息被挤掉)
项目里的死信队列配置(RabbitmqServiceImpl.java):
1 | |
死信队列的工作流程:
1 | |
面试回答:
“死信队列是消息处理失败后的兜底方案。我们项目里,消费者处理失败时
basicNack(requeue=false),消息自动进入死信队列。后续人工排查失败原因(可能是代码 Bug 或数据问题),修复后从死信队列重新消费。相当于给消息一个’复活’的机会。”
Q10:死信队列里的消息应该怎么处理?能不能自动重试?
答: 死信队列里的消息通常不能自动重试(因为上次消费失败了,重试大概率还会失败),需要人工介入。
处理死信队列的三种策略:
策略 1:人工排查 + 手动重发(最常用)
1 | |
策略 2:延迟自动重试(谨慎使用)
1 | |
策略 3:报警 + 日志记录
1 | |
面试回答:
“死信队列里的消息一般不能自动重试,因为上次已经失败了,重试大概率还会失败。我们的处理方式是:① 死信队列有消息进入时发报警(钉钉/邮件);② 人工排查失败原因(代码 Bug 还是数据问题);③ 修复后手动重发到主队列。如果非要自动重试,要限制重试次数(比如 3 次),超过后转人工处理。”
第五层:RabbitMQ 高可用与运维
Q11:RabbitMQ 怎么保证高可用?如果单台 RabbitMQ 宕机了怎么办?
答: 单台 RabbitMQ 是单点故障,要用镜像队列(Mirrored Queue) 或 Quorum Queue 实现高可用。
镜像队列(RabbitMQ 3.8 之前的方式):
1 | |
Quorum Queue(RabbitMQ 3.8+ 推荐):
1 | |
项目里的配置: 从代码看,项目用的是单台 RabbitMQ(switchFlag=false 默认关闭),生产环境应该开启镜像队列或 Quorum Queue。
面试回答:
“单台 RabbitMQ 是单点故障,生产环境要用镜像队列或 Quorum Queue。我们项目是个人学习项目,用的是单台 RabbitMQ;如果上生产,会配置镜像队列(所有节点的队列数据一致,主节点宕机后从节点自动接管)。新版本的 RabbitMQ 推荐用 Quorum Queue,基于 Raft 协议,数据强一致。”
Q12:RabbitMQ 的消息积压(堆积)了怎么办?
答: 消息积压说明消费者处理速度 < 生产者发送速度,要扩容消费者或优化消费逻辑。
导致积压的原因:
- 消费者处理能力不足(比如消费者只有 1 个,但消息量是 1 万/秒)
- 消费者挂了(所有消费者都宕机了,消息只进不出)
- 消费逻辑太慢(比如每条消息要处理 5 秒,但每秒来 100 条)
解决方案:
1. 扩容消费者(最直接)
1 | |
2. 优化消费逻辑(提升单消费者处理能力)
1 | |
3. 紧急预案:新建一个”加速队列”
1 | |
面试回答:
“消息积压说明消费者速度跟不上生产者。解决方案:① 扩容消费者(启动多个消费者实例,RabbitMQ 会自动负载均衡);② 优化消费逻辑(比如批量处理代替逐条处理);③ 紧急情况可以新建一个’加速队列’,分配更多消费者去处理积压消息。”
第六层:项目代码细节与开放题
Q13:你们项目里 RabbitMQ 的 switchFlag 是 false,默认是关闭的,为什么?
答: 因为本地开发不一定安装 RabbitMQ,关闭时走 Spring 内部的事件总线(ApplicationEventPublisher),功能等价,但不需要依赖外部中间件。
代码里的实现(UserFootServiceImpl.java):
1 | |
Spring 事件总线的原理:
1 | |
为什么本地开发不用 RabbitMQ?
- 安装 RabbitMQ 麻烦(要装 Erlang,要配置)
- 本地调试时,直接用 Spring 事件总线更简单(不用启动 RabbitMQ)
- 部署到服务器时,再把
switchFlag改为true
面试回答:
“我们项目
switchFlag默认是false,因为本地开发不一定安装 RabbitMQ。关闭时走 Spring 内部的ApplicationEventPublisher(事件总线),功能和 MQ 等价,但不需要依赖外部中间件。部署到测试/生产环境时,再把switchFlag改为true,走真正的 RabbitMQ。”
Q14:如果让你设计一个”点赞通知系统”,日活 100 万,你会怎么设计?
答: 这是一道系统设计题,要综合考虑性能、可靠性、成本。
V1(最简单):RabbitMQ 直接异步处理
1 | |
问题:如果日活 100 万,平均每人点赞 2 次 → 每天 200 万条通知消息,RabbitMQ 能扛住,但通知表会很大。
V2(引入批量处理):消费者每积攒 100 条通知,再批量 INSERT
1 | |
问题:如果消费者宕机,内存里的通知就丢了。
V3(最终方案):RabbitMQ + 批量 + 定时任务兜底
1 | |
面试回答模板:
“日活 100 万的点赞通知系统,我会这样设计:① 用户点赞立即写 DB 并返回,同时发消息到 RabbitMQ;② 消费者积攒 100 条或每隔 5 秒批量 INSERT 通知表(减少 DB 压力);③ 消费者宕机后,RabbitMQ 的手动 Ack 保证消息不丢,其他消费者接手;④ 定时任务兜底,每天凌晨扫描’有点赞记录但没通知’的数据,补生成。这样就兼顾了性能(批量 INSERT)和可靠性(RabbitMQ 手动 Ack + 定时任务兜底)。”
Q15(附加):RabbitMQ 的消息是有序的吗?怎么保证顺序消费?
答: RabbitMQ 不保证全局有序,但可以保证同一个队列里的消息按 FIFO 顺序被同一个消费者消费。
问题场景:
1 | |
保证顺序消费的方案:
方案 1:同一个订单的消息,始终路由到同一个队列(推荐)
1 | |
这样,同一个订单的所有消息都会到同一个队列,被同一个消费者顺序处理。
方案 2:用 RabbitMQ 的单个消费者 + 单线程处理**
1 | |
面试回答:
“RabbitMQ 不保证全局有序。要保证顺序消费,可以用’相同业务 ID 路由到同一个队列’的方案,比如根据订单 ID 的 hash 值路由,这样同一个订单的消息始终被同一个消费者顺序处理。或者简单粗暴地只用 1 个消费者,但性能差。我们项目里点赞通知不需要保证顺序,所以没有做这个优化。”
总结:简历上这句话的面试回答模板
面试官:”你的简历上写了’RabbitMQ 异步解耦,手动 Ack 保证消息至少处理一次,Confirm 和死信队列兜底’,你能详细讲一下吗?”
回答模板(背下来!):
“好的。我们项目里点赞操作后需要生成通知消息,如果同步执行,用户要等通知生成完才能继续操作,体验不好。所以用 RabbitMQ 做异步解耦:用户点赞后立即返回,通知消息异步处理。
生产端可靠性:我们开启了 Confirm 机制,
channel.confirmSelect()后,RabbitMQ 成功持久化消息才会异步回调handleAck,如果 Broker 内部错误会回调handleNack,我们在 nack 回调里记录日志并准备重发。同时用waitForConfirmsOrDie(3000)同步等待确认,3 秒超时算发送失败。消费端可靠性:我们关闭了自动 Ack(
autoAck=false),等业务逻辑处理完成后才调用basicAck。这样如果消费者处理中途宕机,RabbitMQ 会把消息重新分发给其他消费者,保证 at-least-once。重复消费问题,我们用 notify 表的唯一索引(user_id + article_id + type)做幂等,重复插入时捕获 DuplicateKeyException 直接忽略。死信队列兜底:消费者处理失败时,我们用
basicNack(requeue=false)让消息进入死信队列(DLQ)。后续人工排查失败原因,修复后手动重发到主队列。相当于给消息一个’复活’的机会,不会丢失。整体来说,这套方案保证了点赞通知的高性能(异步解耦)和高可靠(Confirm + 手动 Ack + 死信队列兜底)。”
下一篇预告: 策略模式 + 工厂模式实现 LLM 调用链路统一抽象,以及 WebSocket 推送 Stream 流实现毫秒级响应 🔜