# 07.面试八股-RabbitMQ
# 🔥 话题一:MQ 作用(异步、解耦、削峰)
# 🟢 第一问
面试官:为什么要在系统里引入消息队列?说出三个典型场景。
💡 答案要点
| 作用 | 说明 | 举例 |
|---|---|---|
| 异步 | 非核心逻辑不阻塞主流程 | 订单创建后发送短信/邮件 |
| 解耦 | 生产者不关心谁消费 | 订单完成后,库存/积分/物流各自订阅 |
| 削峰 | 缓冲突发流量,保护下游 | 秒杀场景,下单请求先入队,消费者慢慢处理 |
# 🟡 第二问(追问)
面试官:异步一定能提升性能吗?有没有副作用?
不一定,异步不是提升单任务执行速度,而是通过非阻塞提升系统吞吐量;CPU 密集型、简单短任务用异步反而会增加线程切换和调度开销,降低性能。
入队出队延迟变高了
链路变复杂了,排查问题更难
上下文开销变大
# 🔥 话题二:消息丢失(重点中的重点)
# 🟢 第一问
面试官:消息从生产到消费,在哪些环节可能丢失?怎么防止?
三个阶段都可能丢:
| 阶段 | 丢消息原因 | 解决方案 |
|---|---|---|
| 生产 → MQ | 网络抖动,发送方以为成功但 MQ 没收到 | 生产者确认机制(publisher confirm) |
| MQ 存储 | MQ 宕机,内存中的消息丢了 | 持久化 + 主从/集群 |
| MQ → 消费者 | 消费者拿到消息还没处理完就自动 ACK | 手动 ACK,处理完再确认 |
# 🟡 第二问(追问)
面试官:RabbitMQ 怎么开启生产者确认?和事务的区别是什么?
只需在 Channel 上调用 confirmSelect() 方法即可。
- 普通 Confirm 模式(单条同步等待):每发送一条消息,就调用
waitForConfirms()方法等待服务端的确认。这种方式是同步阻塞的,性能提升有限。 - 批量 Confirm 模式:发送一批消息后,调用一次
waitForConfirms()进行确认。如果返回失败,需要重发这一整批消息。 - 异步 Confirm 模式(生产环境最推荐):提供一个回调函数(ConfirmListener),生产者发送消息后无需阻塞等待,继续发送下一条。当 Broker 成功处理或处理失败时,会异步回调通知生产者。
“RabbitMQ 开启生产者确认只需要在 Channel 上调用 confirmSelect() 方法。在实际生产中,我们通常会使用异步 Confirm 模式,通过注册回调监听器(ConfirmListener)来接收 Broker 返回的 ACK 或 NACK,这样既能保证消息可靠投递,又不会阻塞生产者的发送线程。
关于它和事务的区别,核心在于性能。RabbitMQ 的事务机制是同步阻塞的,发一条消息就要等待一次事务提交响应,这会导致吞吐量急剧下降,完全无法满足高并发的生产需求。而 Publisher Confirm 机制是异步非阻塞的,它允许生产者连续发送消息,通过回调来确认消息状态,吞吐量可以达到事务机制的几十倍甚至上百倍。所以,除非是极低频且对强一致性有严苛要求的特殊场景,否则在生产环境中我们都会选择 Publisher Confirm 来替代事务机制,来保障消息的可靠投递。”
# 🔴 第三问(深度追问)
面试官:消费者手动 ACK 时,如果处理消息过程中宕机了,消息会丢吗?会重复吗?
不会丢失,因为没有确认的情况下,消息队列是不会删除消息的
可能会重复,消息如果没被确认,会返回到队列中,对消息进行二次消费
解决方案:
可以加幂等性判断,在每次消息处理之前,判断根据全局ID判断此条消息是否被处理过,如果处理过直接ACK,未处理就按逻辑进行处理
# 🔥 话题三:消息重复(幂等性)
# 🟢 第一问
面试官:除了消费者宕机,还有哪些场景会导致消息重复?
MQ确认超时,消息处理慢,MQ没接到ACK,重发
生产者发送消息后的确认超时,让生产者以为没收到,重新发送
批量确认消息失败,部分成功的消息,重新发送
# 🟡 第二问(追问)
面试官:有哪些实现幂等的方法?你的项目里怎么做的?
利用数据库主键或唯一索引(Unique Key)的天然特性。
为每条消息分配一个全局唯一的 ID(如
msgId或业务单号)。消费者在处理前,先去 Redis 执行SETNX(或setIfAbsent)操作。在更新核心业务状态时,在 SQL 的
WHERE条件中带上当前的前置状态。例如更新订单状态:UPDATE orders SET status = 'PAID' WHERE order_id = 'xxx' AND status = 'UNPAID'。
# 🔥 话题四:消息顺序
# 🟢 第一问
面试官:RabbitMQ 怎么保证消息的顺序性?
RabbitMQ 保证顺序的核心依然是局部有序’。
最基础的方案是‘单队列 + 单消费者,利用队列天然的 FIFO 特性来保证顺序。
但在高并发场景下,我们会采用分区队列(分片)的策略。比如根据订单 ID 进行哈希,将同一笔订单的所有消息路由到同一个队列,并为每个队列分配独立的单线程消费者。这样既保证了同一订单内的严格有序,又通过多队列并行提高了系统的整体吞吐量。
此外,在使用 RabbitMQ 时我会特别注意消息重试导致的乱序问题。因为 RabbitMQ 失败重试的消息会被扔到队列尾部,极易打乱顺序。所以对于保序消息,我会关闭自动重试入队,配合死信队列来做异常兜底;同时在生产端开启 Publisher Confirm 机制,确保消息按序到达 Broker。
# 🔥 话题五:Nacos(服务注册与发现)
# 🟢 第一问
面试官:服务注册到 Nacos 后,消费者怎么知道调用哪个实例?
服务启动,注册自己的服务信息到Nacos
服务从Nacos注册中心拉取服务列表
通过负载均衡选择一个实例来调用
# 🟡 第二问
面试官:Nacos 的临时实例和永久实例有什么区别?
健康检测机制不同,Nacos临时实例中,客户端主动上报心跳,如果超时则被自动剔除,在永久实例中,服务端主动探测,如果实例宕机,会标记为不健康,不会剔除
存储方式不同,临时实例存储在内存中,每次重启都重新加载,永久实例在磁盘中,重启后依然存在
临时实例适合普通微服务业务
永久实例适合Mysql,Redis等长期稳定的基本业务
# 🔥 话题六:OpenFeign
# 🟢 第一问
面试官:Feign 的原理是什么?怎么实现远程调用像本地调用一样?
在应用启动阶段,Feign会自动扫描带有@FeignClient的注解,自动将他们创建为代理对象注册到Spring容器中
在实际应用阶段,当我们调用接口方法时,代理对象会拦截这次请求,解析请求中的@GetMapping之类的注解和参数相关注解,动态构建出一个HTTP请求,结合路径和负载均衡发送到真实的服务中,调用,返回json,并解析为java对象
# 🟡 第二问(追问)
面试官:Feign 调用超时怎么配置?为什么要配?
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时
readTimeout: 5000 # 读取超时
2
3
4
5
6
- 原生 Feign(脱离 Spring Cloud 单独使用):
- 连接超时(Connect Timeout):默认 10秒
- 读取超时(Read Timeout):默认 60秒 (实现快速失败,防止占用CPU)
- Spring Cloud 环境下的 Feign(实际项目中最常见):
- 由于 Feign 集成了 Ribbon(或 LoadBalancer)作为负载均衡组件,Ribbon 的默认配置会覆盖 Feign 的原生配置。
- 连接超时(Connect Timeout):默认 1秒
- 读取超时(Read Timeout):默认 1秒(适配一部分慢接口)
# 🔥 话题七:Gateway(路由、限流、降级)
# 🟢 第一问
面试官:Gateway 的核心组成是什么?一个请求进来是怎么处理的?
Router(路由):id+uri+predicate(断言:匹配条件,如:路径/请求头)+filter
filter(过滤器):请求前/后处理
请求 ==> 网关 Handler Mapping (根据路径匹配Router) ==> FilteringWebHandler(加载并调用路由下的过滤器链) ==> 执行过滤器链 ==> 转发到相应的服务
# 🟡 第二问(追问)
面试官:Gateway 怎么实现限流?
Gateway 内置限流(基于 Redis + 令牌桶):
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒令牌数
redis-rate-limiter.burstCapacity: 20 # 最大令牌数
2
3
4
5
6
7
8
9
10
11
# 🟢 第三问
面试官:服务熔断和降级的区别?
熔断更像一种自我保护机制,他关注的是自我稳定性。当下游服务故障时,熔断器会自动断开,防止因为故障发生雪崩,将服务设置为半开半关状态可自动恢复
降级更像是一种兜底策略,关注用户体验,当系统流量过大时,主动关闭非核心功能,比如评论、推荐,优先保全核心功能支付下单
在实际项目中,通常配合使用,触发熔断后设置一个降级策略兜底。