消息队列面试八股文(深度版)|RabbitMQ + Kafka 30题讲透

写在前面:本文用最通俗的语言 + 生活化比喻,把消息队列的每个核心知识点讲透。读完后,你不仅能应对面试,还能真正理解 MQ 的设计思想。


一、为什么要用消息队列?(三板斧:解耦、异步、削峰)

第1题:你们项目为什么要用消息队列?

一句话总结:消息队列解决的是系统耦合、响应慢、扛不住峰值三大问题,核心能力是解耦、异步、削峰

深度解析

用生活场景来理解。假设你开了一家餐厅:

没有消息队列(灾难现场)

1
2
3
顾客点餐 → 你亲自去厨房传菜 → 等厨房做好 → 你亲自端给顾客

厨房忙的时候,你只能干等,后面排队的顾客全炸了

有了消息队列(优雅解法)

1
2
3
顾客点餐 → 把订单放到"订单柜"(消息队列)→ 立刻告诉顾客"已下单"

厨房按自己的节奏从"订单柜"取订单,慢慢做

这三个好处分别对应:

好处 比喻 技术解释
解耦 餐厅和后厨通过”订单柜”解耦,互相不依赖 系统A不直接调用系统B,通过MQ传递消息,A和B完全独立
异步 顾客下单后不用等,可以先去找座位 用户请求立刻返回,耗时操作(发邮件、生成报表)异步处理
削峰 中午高峰期订单柜可以积压订单,厨房按能力处理 秒杀场景每秒1万请求,MQ拦住,系统按每秒1000的速度处理

面试加分回答

“我们在电商下单场景用了MQ。用户下单后,需要扣库存、发短信、发邮件、生成物流单,如果同步做,用户要等3秒。引入MQ后,下单操作只把消息发给MQ(20ms),立即返回用户’下单成功’,后续操作异步处理,用户体验提升10倍。另外在秒杀场景,MQ帮我们扛住了瞬时万级QPS的流量洪峰。”


二、消息队列有什么优缺点?

第2题:消息队列有什么优缺点?要不要上MQ?

一句话总结:MQ 能解耦异步削峰,但引入了系统复杂性、一致性问题、可用性风险,需要权衡。

深度解析

优点 具体说明
解耦 系统间通过消息通信,不用互相调用
异步 用户不用等后台操作完成
削峰 缓冲瞬时高流量,保护后端系统
冗余 消息可以持久化,防止数据丢失
缺点 具体说明 怎么解决
系统可用性降低 MQ挂了,所有依赖的系统都挂了 搭建MQ集群,保证高可用
系统复杂性增加 要考虑消息丢失、重复消费、顺序性问题 针对性设计方案(后面详细讲)
一致性问题 A系统处理成功了,BCD系统可能失败 分布式事务(最终一致性)

面试加分回答

“我们评估是否引入MQ有三个标准:① 是否有异步场景(如发短信、日志记录)② 是否要解耦多个系统 ③ 是否有流量峰值需要缓冲。三个都不满足就不引入,因为MQ带来的复杂性成本很高。我们有一次就是因为滥用MQ,导致消息积压百万条,反而成了事故。”


三、Kafka 架构原理(必考!)

第3题:Kafka 的架构是怎么设计的?为什么这么快?

一句话总结:Kafka 通过分布式分区 + 顺序写磁盘 + 零拷贝 + 批量处理,实现了每秒百万级的吞吐量。

深度解析

先理解 Kafka 的核心概念(用食堂打饭比喻):

1
2
3
4
5
6
7
Kafka Cluster(食堂)
├── Broker(打饭窗口):每台服务器是一个Broker
├── Topic(菜品类型):每种菜是一个Topic,比如"红烧肉Topic"
├── Partition(打饭队伍):每个Topic分多个Partition,相当于多个队伍同时打红烧肉
├── Producer(打饭的人):往Topic里发消息
├── Consumer(吃红烧肉的人):从Topic里取消息
└── Consumer Group(拼桌吃饭的小组):一组消费者共同消费一个Topic

Kafka 为什么这么快?(四个核心技术)

① 顺序写磁盘(比内存随机写还快!)

1
2
3
4
5
随机写:每次写都要"寻址" → 像在图书馆随机找书,累死
顺序写:一直往后追加 → 像在笔记本上一直往下写,飞快

Kafka把消息按顺序追加到磁盘文件末尾,省去了磁盘寻址时间
实测:顺序写磁盘 600MB/s,随机写只有 100KB/s,差6000倍!

② 零拷贝(Zero Copy)(省去4次拷贝中的2次)

1
2
3
4
5
6
7
8
9
传统网络发送文件(4次拷贝):
磁盘 → 内核缓冲区 → 应用程序缓冲区 → Socket缓冲区 → 网卡

这两次拷贝完全没必要!

零拷贝(sendfile系统调用):
磁盘 → 内核缓冲区 ───────────→ 网卡

数据不用进应用内存,直接在内核搞定!

③ 批量处理(攒一批再发,减少网络开销)

1
2
不用MQ:发1条消息 = 建立连接 + 发送 + 关闭连接(网络开销大)
Kafka: 攒1000条消息,压缩后一次性发送(像寄快递,攒一箱再寄)

④ 数据压缩(LZ4、Snappy、Zstd)

1
消息压缩后体积大幅缩小,网络传输更快

面试加分回答

“Kafka 的高性能是四维优化的结果:① 顺序写磁盘规避寻址开销 ② sendfile 零拷贝减少2次内存拷贝 ③ 批量发送+数据压缩降低网络开销 ④ 分区并行处理提升吞吐量。我们线上单机 Kafka 吞吐量能达到 50MB/s,相当于每秒处理 50 万条消息。”


第4题:Kafka 的分区(Partition)有什么用?

一句话总结:Partition 是 Kafka 并行处理 + 负载均衡 + 顺序保证 的核心机制,一个 Topic 可以有多个 Partition,每个 Partition 是一个有序的队列。

深度解析

为什么要有 Partition?

想象一个超市的收银台:

1
2
3
4
5
6
7
8
没有分区(只有1个收银台):
所有顾客排一队 → 再快的收银员也扛不住

有分区(多个收银台):
Partition 0:收银台1
Partition 1:收银台2
Partition 2:收银台3
3个收银台同时工作,吞吐量翻3倍!

Partition 的关键特性

特性 说明
有序性 每个 Partition 内部消息是有序的(像队列 FIFO)
并行度 Partition 数量 = 最大并行消费者数量
副本机制 每个 Partition 可以有多个副本(Replica),高可用
Leader/Follower 每个 Partition 有一个 Leader(处理读写)和多个 Follower(只同步数据)

分区策略(消息发到哪个 Partition?)

1
2
3
4
5
6
// 1. 指定了 key → 相同 key 的消息一定发到同一个 Partition(保证顺序!)
// hash(key) % numPartitions

// 2. 没指定 key → 轮询(Round-Robin),均匀分布

// 3. 自定义分区器 → 想发哪就发哪

面试加分回答

“Kafka 的分区设计是整个架构的精髓。分区数决定了消费的并行度——我们有个 Topic 设置了 12 个分区,对应 12 个消费者实例并行处理,吞吐量直接翻了 12 倍。但要注意:如果需要保证消息的全局顺序,只能设置 1 个分区,这会成为性能瓶颈,所以要权衡顺序性和吞吐量的需求。”


第5题:Kafka 的副本机制(Replica)是怎么保证高可用的?

一句话总结:Kafka 每个 Partition 有多个副本(Replica),分为 Leader(处理读写)和 Follower(只同步),通过 ISR 机制 保证数据不丢失。

深度解析

副本的基本概念

1
2
3
4
5
6
Partition 03 个副本:
├── Leader Replica(领导者):处理所有读写请求
├── Follower Replica(跟随者):从 Leader 拉取数据,不对外服务
└── Follower Replica(跟随者):同上

如果 Leader 挂了 → 从 Follower 中选举新的 Leader(通过 Controller)

ISR(In-Sync Replicas)机制(核心中的核心!):

1
2
3
4
5
6
7
8
9
10
11
12
ISR = 和 Leader 保持同步的副本集合

举例:Partition 03 个副本 [0, 1, 2]
- 0 是 Leader
- 12 是 Follower
- 如果 2 落后太多(超过 replica.lag.time.max.ms = 30s)
2 被踢出 ISR(但还在,只是不同步了)
→ ISR = [0, 1]

acks=all 的含义:
→ 消息要写入 ISR 中所有副本才算"发送成功"
→ 只要 ISR 中还有一个副本存活,数据就不会丢!

acks 参数(决定消息可靠性)

acks 值 含义 可靠性 吞吐量
0 发了就不管了,不等待确认 最低(可能丢消息) 最高
1 只要 Leader 写入成功就返回 中等(Leader 挂了可能丢) 中等
all(推荐) ISR 中所有副本都写入成功 最高 最低

面试加分回答

“Kafka 通过多副本 + ISR 机制保证高可用。我们生产环境设置 acks=all 且 min.insync.replicas=2,确保每条消息至少写入两个副本。另外 Kafka 的 Leader Epoch 机制解决了之前 ISR 切换可能导致的数据不一致问题——每个 Leader 变更都有一个 Epoch 版本号,Follower 同步时会校验 Epoch,避免脏数据。”


四、RabbitMQ 架构原理

第6题:RabbitMQ 的核心架构是什么?Exchange 有哪些类型?

一句话总结:RabbitMQ 通过 Exchange(交换机)→ Queue(队列) 的路由机制,实现灵活的消息分发;Exchange 有 4 种类型,决定消息怎么路由。

深度解析

RabbitMQ 核心组件(用快递站比喻):

1
2
3
4
5
6
7
Producer(寄快递的人)
↓ 发消息(带 routingKey)
Exchange(快递分拣中心)← 决定消息去哪个队列
↓ 根据 Exchange 类型和 Binding 规则
Queue(快递柜)→ 存储消息的地方

Consumer(取快递的人)

4 种 Exchange 类型(面试必考!):

Exchange 类型 路由规则 比喻 使用场景
Direct(直连) routingKey 完全匹配 快递单号精确匹配收件人 点对点通信,指定特定队列
Topic(主题) routingKey 模式匹配* 匹配一个词,# 匹配多个词) 快递地址模糊匹配(”北京.*”) 根据多个条件路由(如 order.createdorder.paid
Fanout(广播) 忽略 routingKey,广播到所有绑定的队列 小区广播,所有快递柜都放一份 发布订阅场景(如新用户注册,发短信+发邮件+发优惠券)
Headers(头匹配) 根据消息 headers 匹配(不用 routingKey) 根据包裹属性匹配(重量>10kg的走特殊通道) 很少用

代码示例(Topic Exchange)

1
2
3
4
5
6
// 发送消息,routingKey = "order.created"
channel.basicPublish("orderExchange", "order.created", null, message);

// Queue1 绑定 key = "order.*" → 能收到 order.created
// Queue2 绑定 key = "order.#" → 能收到 order.created、order.created.by_vip
// Queue3 绑定 key = "*.created" → 能收到 order.created、user.created

面试加分回答

“我们项目用 Topic Exchange 实现订单状态变更的事件通知。订单创建、支付、发货分别对应 routingKey:order.createdorder.paidorder.shipped,不同的下游系统(库存系统、物流系统、积分系统)只绑定自己关心的 routingKey,实现精准消费,避免了 Fanout 所有系统都收到所有消息的浪费。”


第7题:RabbitMQ 怎么保证消息不丢失?

一句话总结:消息不丢失需要从 三个环节 同时保证:① 生产者确认(Confirm 机制)② 消息持久化(Exchange + Queue + Message)③ 消费者确认(Ack 机制)。

深度解析

消息从生产到消费的完整链路:

1
2
生产者 → Exchange → Queue → 消费者
① ② ③ ④

每个环节都可能丢消息,需要针对性保护

① 生产者 → Exchange(网络断了怎么办?)

1
2
3
4
5
6
7
8
9
10
11
12
// 开启 Confirm 机制(生产者确认)
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) {
// 消息成功到达 Exchange
}
@Override
public void handleNack(long deliveryTag, boolean multiple) {
// 消息丢失了!需要重发
}
});

② Exchange → Queue(Queue 挂了怎么办?)

1
2
3
4
5
6
7
8
9
10
11
12
// Exchange 和 Queue 都要持久化
// 1. 声明持久化 Exchange
channel.exchangeDeclare("orderExchange", "topic", true); // durable=true

// 2. 声明持久化 Queue
channel.queueDeclare("orderQueue", true, false, false, null); // durable=true

// 3. 发送持久化消息
AMQP.BasicProperties props = MessageProperties.PERSISTENT_TEXT_PLAIN;
channel.basicPublish("orderExchange", "order.created", props, message);
// ↑
// deliveryMode=2 表示消息持久化

③ Queue → 消费者(消费者挂了怎么办?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 关闭自动 Ack,手动确认(确保处理完再 Ack)
channel.basicConsume("orderQueue", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
try {
// 处理业务逻辑...
processMessage(body);
// 手动 Ack(告诉 RabbitMQ 消息处理完了,可以删了)
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 处理失败,拒绝消息(可以重新入队或丢弃)
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
});

三个环节的保护总结

环节 风险 解决方案
生产者发消息 网络抖动,消息没到 Exchange Confirm 机制(异步确认)
Exchange 路由 Exchange/Queue 没持久化,重启后消息丢失 Exchange、Queue、Message 全部设置 durable=true
消费者处理 消费者拿到了消息但处理前挂了 关闭自动 Ack,手动 Ack

面试加分回答

“保证消息不丢失我们做了全链路保护:生产端开启 Confirm 回调,失败重试;服务端 Exchange、Queue、Message 全部开启持久化(注意:只设置 durable=true 不够,发送消息时还要设置 deliveryMode=2);消费端关闭自动 Ack,等业务逻辑执行成功后手动 basicAck。另外我们还设置了 mirrored-queue(镜像队列),主节点挂了从节点自动接管,进一步保障高可用。”


五、如何保证消息不重复消费(幂等性)?

第8题:消费者怎么保证消息不被重复消费?(幂等性设计)

一句话总结:消息重复消费的根本原因是网络重试 + 消费者重试,解决方法是让消费逻辑具备幂等性(多次执行结果相同),常用方案:唯一ID + 去重表 / Redis / 数据库唯一索引

深度解析

为什么会重复消费?

1
2
3
4
5
6
7
8
场景1:生产者重试
网络抖动 → 生产者没收到 ACK → 以为消息丢了 → 重发 → 消费者收到2条相同消息

场景2:消费者重试
消费者处理了消息,但发送 ACK 前挂了 → RabbitMQ 认为没消费成功 → 重新投递 → 消费者又处理一次

场景3:Kafka 重平衡
Consumer Group 重平衡时,某个 Partition 的消费者切换 → 新的消费者可能重新消费上次的消息

幂等性解决方案(重点!):

方案1:数据库唯一索引(最常用)

1
2
3
4
5
6
7
8
9
10
-- 消息消费记录表
CREATE TABLE message_consumed (
message_id VARCHAR(64) PRIMARY KEY, -- 消息唯一ID(生产者生成)
consumed_at DATETIME DEFAULT NOW()
);

-- 消费时先插入,如果重复会报主键冲突,捕获异常跳过
INSERT INTO message_consumed(message_id) VALUES (?);
-- 如果插入成功 → 处理业务逻辑
-- 如果主键冲突 → 说明已经消费过,直接跳过

方案2:Redis SetNX(高性能)

1
2
3
4
5
6
7
8
// 用消息唯一ID作为Key,SetNX保证只有第一次能设置成功
String result = jedis.set(messageId, "1", "NX", "EX", 86400);
if ("OK".equals(result)) {
// 第一次消费,处理业务逻辑
processMessage(message);
} else {
// 已经消费过,跳过
}

方案3:乐观锁(更新场景)

1
2
3
4
5
-- 消息里带上版本号,更新时带上版本号条件
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = 1001 AND version = 5;
-- 如果 version 已经被其他消费修改了,这条 SQL 影响行数=0,更新失败

面试加分回答

“我们保证幂等性的策略是根据业务场景选择的:如果是新增操作,用数据库唯一索引 + 状态机(status 从 INIT 变为 PROCESSING 用 CAS 更新,只有状态符合预期才处理);如果是更新操作,用乐观锁 version 字段;如果是纯查询,天然幂等不用处理。另外我们在消息体里强制要求生产者带上业务唯一ID(如 order_id + 操作类型),这样消费端才能做去重判断。”


六、如何保证消息的顺序性?

第9题:怎么保证消息按顺序消费?

一句话总结:顺序性保证的核心是让需要保证顺序的消息进入同一个分区/队列,且由一个消费者单线程消费

深度解析

为什么消息会乱序?

1
2
3
4
5
问题场景(Kafka):
Topic3Partition
订单ID=1001 的操作:创建 → 支付 → 发货
3条消息被发到了不同的 Partition
→ 消费者并行处理,可能"发货""创建"先执行!

Kafka 的顺序性保证方案

1
2
3
4
5
6
7
8
方案:相同订单ID的消息,用订单ID作为 Key 发送
Kafka 根据 hash(Key) % Partition数 决定分区
→ 相同订单ID一定进入同一个 Partition
Partition 内部是有序的(FIFO
→ 消费者单线程消费这个 Partition → 顺序保证!

注意:如果消费者是多线程处理,还是可能乱序!
所以要保证:同一个 Partition 的消息由同一个线程按顺序处理

RabbitMQ 的顺序性保证方案

1
2
3
4
RabbitMQ 要保证顺序更简单:
1. 使用单个 Queue(不用多个消费者并发消费)
2. 或者多个 Queue,但相同业务ID的消息发到同一个 Queue
3. 每个 Queue 只配置一个消费者(单线程消费)

面试加分回答

“我们电商场景的订单状态变更消息必须保证顺序。解决方案是:Kafka 发送消息时用订单ID作为 Key,确保相同订单的所有消息进入同一个 Partition;消费端我们用单线程消费每个 Partition,并且用内存队列做二级缓冲——拉取消息的线程把消息按 Key 分发给不同的内存队列,每个内存队列由一个工作线程按顺序处理,这样既能保证顺序,又能提升并行度。”


七、大量消息积压怎么办?

第10题:消息队列积压了百万条消息,怎么处理?

一句话总结:消息积压的核心解决思路是先恢复消费速度,再补偿历史数据——① 紧急扩容消费者 ② 提高单消费者吞吐量 ③ 补偿积压数据。

深度解析

积压的原因

1
2
3
原因1:消费者处理速度 < 生产速度(消费者代码慢 / 数据库慢)
原因2:消费者挂了(代码有Bug,一直报错,消息一直重新入队)
原因3Partition 数太少,消费者无法并行扩展

解决方案(分步骤)

步骤1:紧急恢复(先让消费跟上生产)

1
2
3
① 临时扩容消费者实例(Kafka可以扩到和Partition数一样多)
② 检查消费者是否有Bug,修复后重新部署
③ 如果是数据库慢,给消费者数据库加索引、读写分离

步骤2:处理积压数据(历史数据怎么消费完?)

1
2
3
4
5
6
7
8
9
10
场景:积压了1000万条消息,按现在的速度要消费10小时

方案A:新建一个"临时Topic",设置更多Partition(如64个)
→ 写一个临时分发程序,从旧Topic读取消息,按Key分发到新Topic
→ 启动64个消费者并行消费新Topic
→ 消费完后,切换回原Topic

方案B:如果可以跳过历史数据
→ 直接把消费者组的 offset 跳到最新(相当于丢弃历史消息)
→ 适用于"历史数据不重要"的场景

步骤3:预防(以后怎么避免?)

1
2
3
4
① 监控队列长度,超过阈值告警
② 消费者处理加超时时间,别让一条消息卡住所有消息
③ 设置消息过期时间(TTL),过期自动丢弃
④ Partition 数设计要有余量(未来可以扩消费者)

面试加分回答

“我们有一次 Kafka 积压了 500 万条消息,原因是消费者连接池满了,大量消息处理超时。我们的处理步骤是:① 先紧急扩容消费者从 3 台扩到 12 台(Partition 数是 12)② 修复连接池配置问题重新发布 ③ 新建一个 24 Partition 的临时 Topic,写程序把积压消息重新分发到临时 Topic,24 个消费者并行消费,2 小时消费完 ④ 消费完后切回原 Topic。事后我们加了 Kafka Lag 监控,超过 10 万条就告警。”


八、Kafka 消费者组(Consumer Group)重平衡

第11题:Kafka 的消费者组重平衡(Rebalance)是什么?有什么问题?

一句话总结:Rebalance 是 Consumer Group 成员发生变化时(新增/下线/崩溃),重新分配 Partition 给消费者的过程;Rebalance 期间所有消费者停止消费(Stop-the-world),要尽量避免。

深度解析

什么时候会触发 Rebalance?

1
2
3
4
5
触发条件1:新的消费者加入 Group(扩容)
触发条件2:消费者主动退出(优雅关闭)
触发条件3:消费者崩溃(心跳超时,session.timeout.ms)
触发条件4:Topic 的 Partition 数量变化
触发条件5:消费者长时间不消费(max.poll.interval.ms 超时)

Rebalance 的问题(Stop-the-world)

1
2
3
4
Rebalance 期间:
→ 所有消费者停止消费(像垃圾回收的 Stop-the-world)
→ 如果消费者很多、Partition 很多,Rebalance 可能持续数秒甚至数十秒
→ 这段时间消息堆积!

怎么避免频繁的 Rebalance?

1
2
3
4
5
6
7
8
9
10
11
12
13
① 合理设置 session.timeout.ms(心跳超时,默认10s)
→ 网络偶尔抖动别误判消费者死了

② 合理设置 max.poll.interval.ms(两次 poll 的最大间隔)
→ 如果一次 poll 处理时间很长,要调大这个值
→ 否则会被认为消费者"死了",触发 Rebalance

③ 消费者处理速度要跟上
→ 如果处理太慢,一直超时,就会一直 Rebalance

④ Kafka 0.11 之后引入 Sticky Partition Assignment
→ Rebalance 时尽量保持原来的 Partition 分配不变
→ 减少 Rebalance 的代价

面试加分回答

“Rebalance 是 Kafka 的痛点,我们线上遇到过因为消费者处理时间过长(数据库慢查询)导致频繁 Rebalance 的问题。解决方案是:① 调大 max.poll.interval.ms 从默认 5 分钟到 10 分钟 ② 优化消费者处理逻辑,把耗时操作(如批量数据库插入)改成批量 + 异步 ③ 升级到 Kafka 2.4+,使用 Cooperative Sticky Assignor,Rebalance 时只重新分配有变化的 Partition,不用全部停掉。另外我们监控了 Rebalance 发生的频率和时长,作为 Kafka 健康度的重要指标。”


九、RabbitMQ 的死信队列和延迟队列

第12题:RabbitMQ 的死信队列(DLQ)是什么?怎么用?

一句话总结:死信队列(Dead Letter Queue)是用来存放被拒绝、过期、队列满时被丢弃的消息的队列,常用于异常消息隔离延迟队列的实现。

深度解析

什么情况下消息会变成”死信”?

1
2
3
情况1:消息被拒绝(basic.reject / basic.nack)且不重新入队(requeue=false
情况2:消息过期(设置了 TTL,超时没人消费)
情况3:队列满了(x-max-length),新消息把旧消息挤出去了

死信队列的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 声明一个普通队列,并指定它的死信交换机
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange"); // 死信交换机
args.put("x-dead-letter-routing-key", "dlx.key"); // 死信 routingKey
args.put("x-message-ttl", 10000); // 消息10秒过期

channel.queueDeclare("orderQueue", true, false, false, args);

// 2. 声明死信交换机和死信队列
channel.exchangeDeclare("dlx.exchange", "topic");
channel.queueDeclare("dlx.queue", true, false, false, null);
channel.queueBind("dlx.queue", "dlx.exchange", "dlx.key");

// 效果:
// orderQueue 里 10 秒没人消费的消息 → 自动转发到 dlx.exchange → dlx.queue
// 可以专门有个消费者监听 dlx.queue,处理这些"死信"(如告警、人工介入)

延迟队列的实现(重要应用场景!)

1
2
3
4
5
6
7
8
9
需求:订单下单后 30 分钟未支付,自动取消

实现方案(RabbitMQ 没有原生延迟队列,用 TTL + DLQ 实现):
1. 创建一个"延迟队列"(设置 TTL=30分钟 + DLX 指向"取消订单队列"
2. 订单消息发到延迟队列,30分钟内不消费
3. 30分钟到了,消息过期 → 变成死信 → 转发到"取消订单队列"
4. 消费者从"取消订单队列"取出消息,执行取消订单逻辑

这就是:消息"延迟"30分钟才被消费!

面试加分回答

“我们用死信队列实现了订单超时取消功能。具体做法是:创建一个 TTL=30分钟的延迟队列,订单创建后把消息发到这个队列;消息过期后自动进入死信队列,由取消订单的消费者处理。这里有个坑:RabbitMQ 的 TTL 是队列级别的,如果队列里有一条消息 TTL=1分钟,后面有一条 TTL=1小时,第一条消息过期后,第二条消息虽然没过期也会被一起投递(因为 RabbitMQ 只检查队头消息是否过期)。解决方案是用延迟插件(rabbitmq-delayed-message-exchange),它支持每条消息单独设置延迟时间。”


十、消息队列的选型(RabbitMQ vs Kafka vs RocketMQ)

第13题:RabbitMQ、Kafka、RocketMQ 怎么选型?

一句话总结RabbitMQ 适合轻量级、低延迟场景;Kafka 适合大数据、高吞吐量、日志场景;RocketMQ 适合电商、金融等高可靠场景(阿里开源)。

深度解析

维度 RabbitMQ Kafka RocketMQ
开发语言 Erlang Scala/Java Java
吞吐量 万级 QPS 百万级 QPS 十万级 QPS
延迟 微秒级(最低) 毫秒级 毫秒级
消息可靠性 高(镜像队列) 高(多副本 + ISR) 很高(同步刷盘 + 同步复制)
顺序性 单队列有序 Partition 内有序 单队列有序
延迟队列 原生支持(TTL+DLQ) 不支持(需自己实现) 原生支持
事务消息 不支持 不支持 支持
适用场景 实时性要求高、数据量不大 日志、流式处理、大数据 电商、金融(阿里双11在用)

选型建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
选 RabbitMQ:
→ 中小项目,追求低延迟
→ 需要灵活路由(Exchange 类型丰富)
→ 团队对 Erlang 不畏惧(出问题能排查)

选 Kafka:
→ 大数据场景(日志收集、流式计算)
→ 需要超高吞吐量
→ 和 Flink/Spark Streaming 集成

选 RocketMQ:
→ 电商、金融场景(需要事务消息、延迟消息)
→ 需要顺序消息、消息回溯
→ 想要 Kafka 的吞吐 + RabbitMQ 的功能(RocketMQ 是两者的折中)

面试加分回答

“我们公司的选型策略是:日志收集用 Kafka(吞吐量大,和 ELK 集成方便);核心业务消息用 RocketMQ(支持事务消息,保证消息和本地事务同时成功/失败,适合下单扣库存场景);内部系统解耦用 RabbitMQ(部署简单,延迟低)。另外选型时还要考虑团队技术栈——Kafka 运维复杂度最高,需要专门的 SRE 团队;RabbitMQ 最轻量,小团队也能玩转。”


十一、Kafka 的高水位(HW)和 LEO 是什么?

第14题:Kafka 的 HW(高水位)和 LEO(日志末端位移)是什么?有什么用?

一句话总结LEO 是每个副本最后一条消息的下一个位移(log end offset);HW 是消费者能看到的最新消息位移(所有 ISR 副本都已同步到的最前位置);HW 保证了消费者不会读到未完全同步的消息

深度解析

用图书馆借书来比喻:

1
2
3
4
5
6
7
8
9
LEOLog End Offset):
→ 图书馆最后一本书的"下一本编号"
→ 比如图书馆有 100 本书,LEO = 101

HWHigh Watermark,高水位):
→ 消费者能"借到"的最大编号
→ 只有所有副本都同步到的位置,才能被消费者读取
→ 比如 ISR 中最慢的副本只同步到了 90
HW = 90,消费者只能读到 90 号及之前的书

为什么需要 HW?

1
2
3
4
5
6
7
8
9
10
11
12
没有 HW 的问题:
Leader 写入消息就告诉消费者"可以读了"
→ 消费者读到了消息 101
→ 突然 Leader 挂了,Follower 只同步到 100
→ Follower 成为新 Leader,消息 101 丢了!
→ 消费者:???我读到的消息怎么没了?

有 HW 的保护:
→ 消息 101 写入 Leader,但 HW 还是 100(Follower 没同步到 101
→ 消费者读不到 101,只能读到 100
Leader 挂了,Follower 成为新 Leader,数据没丢
→ 等 Follower 都同步到 101 后,HW 更新为 101,消费者才能读到

LEO 和 HW 的关系

1
2
3
4
5
6
7
8
9
10
11
HW = min(所有 ISR 副本的 LEO)
→ 取 ISR 中最慢的那个副本的 LEO 作为 HW

举例:
Partition3 个副本,LEO 分别是:
Leader: LEO = 105
Follower1: LEO = 105 (同步正常)
Follower2: LEO = 102 (同步较慢,但还在 ISR 里)

HW = min(105, 105, 102) = 102
→ 消费者只能消费到 offset=101(因为 HW=102,能读的范围是 0~101

面试加分回答

“Kafka 的 HW 机制是保证数据一致性的关键。值得注意的是,旧版本 Kafka 的 HW 更新机制有一个缺陷:如果 Leader 切换时 HW 还没有及时更新,可能导致数据不一致甚至数据丢失(这就是所谓的 ‘HW 截断’ 问题)。Kafka 0.11 引入了 Leader Epoch(每个 Leader 版本号),彻底解决了这个问题——Follower 同步时会带上 Leader Epoch,发现 Epoch 不一致就先截断自己的日志到 HW,再同步新 Leader 的数据,保证了数据一致性。”


十二、如何保证消息队列的高可用?

第15题:如何搭建高可用的消息队列集群?

一句话总结:高可用需要从集群架构 + 数据副本 + 监控告警三个维度保障;RabbitMQ 用镜像队列,Kafka 用多副本 + ISR

深度解析

RabbitMQ 高可用方案

1
2
3
4
5
6
7
8
9
10
普通模式(单机):
→ 一台 RabbitMQ 挂了,整个系统不可用 ❌

镜像模式(高可用,推荐):
→ 每个 Queue 的数据在多个节点上都有副本
→ 一个节点挂了,其他节点上的副本自动接管
→ 设置方式:通过 policy 指定哪些队列要镜像、镜像到几个节点

rabbitmqctl set_policy ha-all "^order\." '{"ha-mode":"all"}'
→ 所有以 "order." 开头的队列,镜像到所有节点

Kafka 高可用方案

1
2
3
4
5
6
Kafka 的高可用是天生的(设计时就考虑了):
1. 每个 Partition 有多个副本(通常 3 个)
2. 副本分布在不同的 Broker 上(防止一台机器挂了全挂)
3. Leader 挂了,Controller 从 ISR 里选新 Leader
4. min.insync.replicas=2(写入时至少 2 个副本确认)
→ 哪怕一个 Broker 挂了,数据也不丢

高可用的监控指标

1
2
3
4
5
6
7
8
9
10
RabbitMQ 监控:
→ 队列长度(消息积压)
→ 内存使用率(超过 40% 会开始把消息写到磁盘)
→ 磁盘剩余空间

Kafka 监控:
→ Consumer Lag(消费者落后多少条)
→ ISR 数量(有没有副本被踢出 ISR)
Broker 数量(有没有 Broker 掉线)
→ 消息写入延迟

面试加分回答

“我们 Kafka 集群的高可用配置是:每个 Topic 3 个副本,min.insync.replicas=2,acks=all,这样哪怕同时挂 1 台 Broker 数据也不丢。另外我们用了 Kafka 的 Cruise Control 做自动负载均衡,防止某些 Broker 磁盘用满。监控方面,我们采集了 Consumer Lag、ISR 变更、Broker 存活三个核心指标,配置了分级告警——Lag 超过 10 万条发普通告警,超过 100 万条发紧急告警直接打电话。”


十三、Kafka 的消息投递语义(At-most-once / At-least-once / Exactly-once)

第16题:Kafka 怎么保证精确一次(Exactly-once)语义?

一句话总结:Kafka 通过 幂等性 Producer + 事务(Transaction) 实现精确一次语义;Exactly-once 意味着消息不丢不重,是消息队列的终极目标。

深度解析

三种消息投递语义

语义 含义 可能问题 适用场景
At-most-once(最多一次) 消息可能丢失,但不会重复 丢消息 日志收集(丢几条没关系)
At-least-once(最少一次) 消息不会丢,但可能重复 重复消费 大部分场景(配合幂等)
Exactly-once(精确一次) 消息不丢不重 实现复杂 金融、计费(钱不能多扣也不能少扣)

Kafka 怎么实现 Exactly-once?

方案1:幂等性 Producer(Kafka 0.11+)

1
2
3
4
5
6
7
8
9
10
开启方式:props.put("enable.idempotence", true);

原理:
→ 每个 Producer 有一个唯一 PID
→ 每条消息带一个序列号(sequence number)
→ Broker 会记录 (PID, Partition) -> lastSeq
→ 如果收到 seq <= lastSeq 的消息,说明是重复的,直接丢弃

局限:只能保证"单个 Producer、单个 Session、单个 Partition"内不重复
Producer 重启后 PID 会变,无法跨 Session

方案2:事务(Transaction)(Kafka 0.11+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
开启方式:
props.put("transactional.id", "my-transactional-id");
producer.initTransactions();

使用事务:
producer.beginTransaction();
try {
producer.send(record1);
producer.send(record2);
producer.commitTransaction(); // 原子提交
} catch (Exception e) {
producer.abortTransaction(); // 原子回滚
}

原子性保证:
→ 要么所有消息都写入成功
→ 要么全部失败(消费者要么看到所有消息,要么一条都看不到)

方案3:消费 + 处理 + 提交 Offset 的原子性(最完整的 Exactly-once)

1
2
3
4
5
6
7
8
9
10
11
12
13
问题:消费者处理了消息,但提交 Offset 前挂了 → 重启后重新消费 → 重复处理

Kafka 的解决方案:把 Offset 和处理结果存在同一个事务里!
→ 用 Kafka 的 Transaction,把"处理业务"和"提交 Offset"放在同一个事务
→ 要么都成功,要么都失败
→ 需要业务存储支持事务(如数据库)

伪代码:
beginTransaction();
consume(message); // 从 Kafka 消费
process(message); // 处理业务(写入数据库)
commitOffset(message); // 提交 Offset(写入 Kafka 的 __consumer_offsets)
commitTransaction(); // 原子提交

面试加分回答

“Kafka 的 Exactly-once 语义有两个层级:① 生产端 Exactly-once(幂等性 Producer,保证消息不重复写入)② 消费 + 处理端 Exactly-once(需要 Kafka 的事务 + 业务端的事务配合)。我们在金融对账场景用了 Exactly-once——消费 Kafka 消息,处理完后把处理结果和 Offset 一起写入数据库(同一个数据库事务),这样如果提交 Offset 失败,数据库事务回滚,消息会被重新消费,但处理结果也不会插入数据库,避免了重复处理。”


十四、高频面试题速查表

第17~30题:消息队列高频面试题精选

第17题:如何保证消息队列的消息不丢失?

:全链路保护——生产端用 Confirm 机制;服务端 Exchange、Queue、Message 全部持久化;消费端手动 Ack。Kafka 还要设置 acks=all 和 min.insync.replicas≥2。


第18题:Kafka 为什么这么快?

:① 顺序写磁盘 ② 零拷贝(sendfile)③ 批量发送 + 数据压缩 ④ 分区并行处理。


第19题:RabbitMQ 的 Exchange 有哪些类型?

:Direct(精确匹配)、Topic(模式匹配)、Fanout(广播)、Headers(头匹配,很少用)。


第20题:Kafka 的 Partition 和 Consumer 的关系?

:一个 Partition 只能被 Consumer Group 中的一个 Consumer 消费;Consumer 数不能多于 Partition 数(多余的 Consumer 闲置)。Partition 数决定了最大并行度。


第21题:什么是 Consumer Group?

:Consumer Group 是 Kafka 提供的单播 + 广播混合模型——同一个 Group 内的 Consumer 共同消费一个 Topic(每条消息只被一个 Consumer 消费,实现队列模型);不同 Group 各自独立消费同一个 Topic(每条消息被多个 Group 消费,实现发布订阅模型)。


第22题:Kafka 的消息是怎么存储的?

:每个 Partition 对应一个日志(Log),Log 分为多个 Segment 文件(如 1GB 一个文件);每个 Segment 包含 .log(消息数据)和 .index(偏移量索引);消费者根据 Offset 通过二分查找快速定位到 Segment,再顺序读取。


第23题:如何保证消息队列的一致性问题?

:用本地消息表 + 定时任务(最大努力通知)+ 事务消息(RocketMQ/Kafka 支持)。核心思想:把”发消息”和”本地事务”放在同一个事务里,要么都成功,要么都失败。


第24题:RabbitMQ 怎么实现延迟队列?

:用 TTL(消息过期时间)+ 死信队列(DLQ)——消息设置 TTL,过期后自动进入死信队列,消费者从死信队列取消息,实现”延迟”消费。或者直接用 RabbitMQ 的延迟插件。


第25题:Kafka 的 ISR、AR、OSR 是什么?

:AR(Assigned Replicas)= 所有副本;ISR(In-Sync Replicas)= 和 Leader 同步的副本集合;OSR(Out-of-Sync Replicas)= 落后太多的副本集合。AR = ISR + OSR。


第26题:RabbitMQ 怎么保证高可用?

:用镜像队列(Mirrored Queue)——每个队列的数据在多个节点上有副本,一个节点挂了其他节点自动接管。注意:镜像队列只是高可用,不是负载均衡(所有写操作还是要经过 Master)。


第27题:Kafka 怎么删除旧数据?

:两种策略——① 按时间删除(log.retention.hours,默认7天)② 按大小删除(log.retention.bytes)。过期数据会被异步删除,不影响读写性能。


第28题:消息队列满了怎么办?

:RabbitMQ 可以设置 x-max-length 限制队列长度,超出时新消息根据 x-overflow 策略处理(丢弃最早的消息或拒绝新消息)。Kafka 可以扩大 Partition 数、增加 Broker、或者设置数据过期时间让旧数据自动删除。


第29题:Kafka 的 zero-copy 是怎么实现的?

:通过 sendfile() 系统调用,数据从磁盘文件直接拷贝到网卡缓冲区,跳过内核缓冲区到应用缓冲区、应用缓冲区到 Socket 缓冲区的两次拷贝,减少 2 次上下文切换和 2 次数据拷贝。


第30题:如果让你设计一个消息队列,你会怎么设计?

:核心组件——① Producer API(支持同步/异步发送、负载均衡)② Broker 集群(支持分区、副本、持久化)③ Consumer API(支持消费者组、Offset 管理)④ 协调器(负责副本选举、消费组协调)。关键设计点:持久化用顺序写、网络用零拷贝、高可用用多副本 + ISR、消费进度用 Offset 管理。可以参考 Kafka 的架构设计。


第 31 题:RocketMQ 事务消息的原理?

一句话总结:RocketMQ 事务消息通过 两阶段提交 + 回查机制 保证本地事务和消息发送的事务一致性。

深度解析

1
2
3
4
5
6
7
8
9
10
11
【流程】
1. 发送"半消息"(对消费者不可见)
2. 执行本地事务
3. 根据本地事务结果,提交或回滚半消息
4. 如果步骤 3 没执行(宕机),RocketMQ 会回查生产者

【回查机制】
→ RocketMQ 定时回查(默认 1 分钟一次)
→ 生产者实现 `checkLocalTransaction` 接口
→ 查询本地事务状态(如:订单是否存在)
→ 返回 Commit 或 Rollback

面试加分回答

“我们用 RocketMQ 事务消息解决’本地事务执行成功,但消息发送失败’的问题。关键点是回查机制——如果服务宕机导致没有提交/回滚半消息,RocketMQ 会主动回查,保证最终一致性。注意:事务消息有时间限制(默认 6 秒),如果回查超时,消息会被丢弃或默认提交。”


第 32 题:订单服务与库存服务怎么保证最终一致性?

一句话总结:用 本地消息表 + MQ 重试RocketMQ 事务消息 保证订单和库存的最终一致性。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
【方案 A:本地消息表】
1. 订单服务:开启本地事务
- 创建订单
-`local_message` 表插入一条"扣库存"消息(在同一事务里)
2. 定时任务扫描 `local_message`(状态=未发送)
- 发送到 MQ
- 发送成功,更新状态=已发送
3. 库存服务消费消息,扣库存

【方案 B:RocketMQ 事务消息】
1. 发送半消息
2. 创建订单(本地事务)
3. 提交半消息
4. 库存服务消费

【防止重复扣库存】
→ 库存服务用幂等性(订单 ID + 操作类型作为唯一 Key)
→ 用 Redis SetNX 或数据库唯一索引

面试加分回答

“我们最终选择的方案是 RocketMQ 事务消息,因为本地消息表的定时扫描有延迟(一般 1 分钟扫一次),而事务消息的回查机制可以在秒级内完成一致性保证。另外,库存扣减一定要幂等——我们用订单 ID 作为唯一 Key,用数据库唯一索引保证,即使消息重复消费也不会超扣。”


第 33 题:Kafka Lag 怎么排查和解决?

一句话总结:Kafka Lag = 消费者落后的消息数;排查 先查消费者是否存活,再查 消费逻辑是否有性能瓶颈

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【排查步骤】
1. 查看 Lag:`./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group`
→ 看每个 Partition 的 Lag 值

2. 如果所有 Partition 的 Lag 都很大:
→ 消费者逻辑太慢(如:数据库写入慢、外部 HTTP 调用慢)
→ 解决:优化消费逻辑、增加消费者实例数

3. 如果某个 Partition 的 Lag 特别大:
→ 数据倾斜(某个 Partition 的数据量远大于其他)
→ 解决:重新设计 Key 的哈希策略,让数据均匀分布

4. 如果消费者挂了:
→ 重启消费者,Kafka 会自动重平衡(Rebalance)

面试加分回答

“我们监控 Kafka Lag 的方式是:用 Burrow(LinkedIn 开源的 Kafka Lag 监控工具)或 Kafka 自带的命令。一旦 Lag 超过阈值(如 1 万),触发告警。如果是消费者处理慢,我们会:① 先紧急扩容消费者实例数 ② 优化消费逻辑(如:批量消费、异步处理、减少数据库写入)③ 如果是数据倾斜,重新设计分区策略。”


第 34 题:MQ 怎么保证消息顺序性(深度版)?

一句话总结Kafka 保证同 Partition 内有序(用 Key 哈希到同一 Partition);RabbitMQ 用单队列 + 单消费者保证。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kafka 保证顺序性】
目标:相同订单 ID 的消息,按顺序消费

方案:
1. 发送消息时,用 订单ID 作为 Key
KafkaDefaultPartitioner 会用 `hash(Key) % partitionCount`
→ 相同 Key 一定进入同一个 Partition

2. 消费者:每个 Partition 用单线程消费
→ 或者:多线程消费时,用 订单ID 做内存队列分片
→ 相同订单 ID 的消息,进入同一个内存队列,由同一个线程处理

RabbitMQ 保证顺序性】
方案:
1. 用单个队列(Queue
2. 单个消费者(或者:多个消费者但用排他锁)
3. 如果要用多个消费者:
→ 用一致性哈希(Consistent HashExchange
→ 相同 Key 路由到同一个队列

面试加分回答

“Kafka 保证顺序性的关键是:相同业务 Key 进入同一 Partition,且消费者单线程消费该 Partition。但有个坑:如果消费者是多线程处理(为了提升吞吐量),多线程之间是无序的。我们的解法是:在每个 Partition 的消费线程里,再用 订单ID 做二级分片(用内存队列数组,hash(订单ID) % N 决定进入哪个队列),每个队列由一个工作线程处理,这样既保证了顺序性,又利用了多线程。”


第 35 题:消息队列的高可用架构怎么设计?

一句话总结:高可用架构 = 集群部署 + 多副本 + 负载均衡 + 降级熔断

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【Kafka 高可用】
1. 多 Broker 集群(至少 3 个 Broker)
2. 每个 Topic 至少 3 个副本(replication.factor=3)
3. min.insync.replicas=2(写入时至少 2 个副本确认)
4. 用 Cruise Control 做自动负载均衡和故障转移

【RabbitMQ 高可用】
1. 镜像队列(Mirror Queue):队列数据复制到多个节点
2. 用 HAProxy 或 Keepalived 做负载均衡
3. 如果主节点挂了,镜像节点自动切换

【降级熔断】
→ 如果 MQ 挂了,走降级逻辑(如:用本地缓存 + 定时批量同步)
→ 用 Hystrix 或 Sentinel 做熔断(MQ 响应超时,直接走降级)

面试加分回答

“我们 Kafka 集群的高可用配置是:3 个 Broker,每个 Topic 3 个副本,min.insync.replicas=2。这样即使挂 1 个 Broker,集群仍然可读写。另外,我们用了 Cruise Control 做自动化运维——它能自动检测 Broker 故障、自动迁移 Leader Partition、自动平衡集群负载,大大降低了运维成本。还有重要的一点:生产者要设置 acks=allretries=Integer.MAX_VALUE,确保消息不丢失。”


总结:消息队列面试核心知识点图谱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
消息队列面试八股文
├── 为什么用MQ(解耦、异步、削峰)
├── 优缺点(系统复杂性 vs 能力)
├── RabbitMQ
│ ├── 架构(Exchange → Queue)
│ ├── Exchange 4种类型
│ ├── 消息不丢失(Confirm + 持久化 + 手动Ack)
│ ├── 死信队列 + 延迟队列
│ └── 镜像队列(高可用)
├── Kafka
│ ├── 架构(Broker、Topic、Partition、Consumer Group)
│ ├── 高性能4板斧(顺序写、零拷贝、批量、压缩)
│ ├── 副本机制(ISR、acks参数)
│ ├── 高水位(HW、LEO)
│ ├── 重平衡(Rebalance)
│ └── Exactly-once(幂等 + 事务)
├── 通用问题
│ ├── 消息不丢失(全链路保护)
│ ├── 幂等性(去重表、Redis、乐观锁)
│ ├── 顺序性(同分区/同队列 + 单线程消费)
│ ├── 消息积压(扩容 + 补偿)
│ └── 选型(RabbitMQ vs Kafka vs RocketMQ)
└── 设计题(设计一个消息队列)

写文章不易,如果觉得有帮助,欢迎分享给更多小伙伴 ⭐


补充篇:BAT 高频遗漏题(31-35题)

第 31 题:RocketMQ 的顺序消息怎么实现?

一句话总结:RocketMQ 通过消息分组(MessageQueue 选择策略) + 消费者单线程保证顺序,生产端把相同 Key 的消息发到同一个 MessageQueue,消费端单线程消费。

深度解析

1
2
3
4
5
6
7
8
9
顺序消息的核心:相同业务 Key 的消息 → 同一个队列 → 单线程消费

【生产端】自定义 MessageQueue 选择策略
→ 相同订单 ID 的消息,都进入同一个 MessageQueue
→ 用订单 ID 做哈希:queueId = hash(orderId) % queueCount

【消费端】单线程消费(多线程会乱序!)
→ 每个 MessageQueue 绑定一个消费线程
→ 不能用多线程并发消费同一个队列

为什么 Kafka 的顺序消息更难?

1
2
3
KafkaPartition 内有序,但:
→ 如果消费者是多线程消费一个 Partition → 乱序!
→ 解决:消费者内部用内存队列分组(相同 Key 进同一个内存队列,由固定线程处理)

面试加分回答

“我们用 RockerMQ 做binlog 同步(数据同步必须保证顺序),方案是:① 生产端用表名+主键作为 Key,哈希到同一个 MessageQueue ② 消费端用 MessageListenerConcurrently 但设置 consumeThreadMax=1(单线程消费)③ 如果消费者挂了,RocketMQ 会自动 Rebalance,新消费者从 Offset 继续消费,顺序仍然保持。”


第 32 题:Kafka 的 Exactly-Once 语义怎么实现?

一句话总结:Kafka 0.11+ 支持 Exactly-Once,通过幂等 Producer + 事务型 Producer + 消费者手动提交 Offset 三方配合实现。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Exactly-Once = At-Least-Once + 幂等性

【方案 A】幂等 Producer(适合生产者重试场景)
→ 开启:props.put("enable.idempotence", true)
→ 原理:每个 Producer 分配一个 PID + 序列号
→ 同一条消息重试时,Broker 识别到相同 PID+序列号 → 不重复写入

【方案 B】事务型 Producer(适合"消费 + 处理 + 写结果" 的原子性)
→ 开启事务:producer.initTransactions()
→ 消费 → 处理 → 写结果到下游 Topicproducer.commitTransaction()
→ 如果中途挂了 → 回滚,Offset 不提交,消费者重新消费

【消费端】必须手动提交 OffsetsetEnableAutoCommit(false)
→ 处理完成后再提交 Offset
→ 如果处理完成但提交前挂了 → 重新消费(At-Least-Once
→ 所以下游必须幂等!

面试加分回答

“Kafka 的 Exactly-Once 不是银弹——它要求消费端下游也必须幂等。我们的实践是:① 生产者开启幂等(enable.idempotence=true)防止网络重试导致重复写入 ② 消费者手动提交 Offset,处理完业务逻辑 + 写数据库成功后才提交 ③ 数据库用唯一索引防重(订单 ID + 消息 ID 作为唯一键)。”


第 33 题:消息队列的”消息路由”和”消息追踪”怎么做?

一句话总结消息路由用 Topic + Tag(RocketMQ)或 Routing Key(RabbitMQ);消息追踪用消息 ID + 分布式链路追踪(SkyWalking/Pinpoint)。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
【消息路由】
RockerMQ:Topic(主题)+ Tag(标签)
→ 一个订单 Topic,Tag = CREATE/PAY/SHIP
→ 消费者可以只订阅 Tag=PAY 的消息

RabbitMQ:Exchange + Routing Key
→ Topic Exchange:路由 Key 做通配符匹配
→ Direct Exchange:精确匹配

【消息追踪】(生产环境必备!)
→ 每条消息分配一个 messageId(全局唯一)
→ 发送时:messageId 写入 DB(状态=SENT)
→ 消费时:根据 messageId 更新 DB 状态(CONSUMED)
→ 超时未消费 → 告警 + 人工介入

【分布式链路追踪】
→ 消息里带入 traceId(和 HTTP 请求打通)
→ SkyWalking/Pinpoint 可以追踪:HTTP 请求 → MQ → 消费 → DB 写入 的完整链路

面试加分回答

“我们生产环境的消息追踪方案:① 发送消息时生成 messageId(用雪花算法),写入 msg_trace 表(状态=SENT)② 消费者拉取消息后,根据 messageId 更新状态=CONSUMED ③ 如果 5 分钟状态还是 SENT,触发告警(可能消费者挂了或消费卡住)④ 用 SkyWalking 打通 traceId,可以在同一个界面看到 HTTP 请求 → MQ 发送 → 消费 → DB 写入的完整链路,排查问题非常方便。”


第 34 题:RabbitMQ 的镜像队列(Mirror Queue)是怎么同步的?

一句话总结:镜像队列是主从同步,Master 节点处理所有读写,Slave 节点只做备份;Master 挂了,最老的 Slave 自动提升为 Master。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【镜像队列的同步过程】
1. 生产者发送消息 → Master 节点
2. Master 节点写入磁盘 + 同步给所有 Slave 节点
3. 所有 Slave 确认收到 → Master 才给生产者返回 ACK
4. 消费者只能从 Master 节点消费

【主从切换】
→ Master 挂了 → 最老的 Slave 提升为 Master
→ 其他 Slave 重新同步新 Master(可能丢数据!)
→ 所以:镜像队列 ≠ 高可用(异步同步时可能丢数据)

【高性能镜像队列】
→ 用 `ha-sync-mode: automatic`(自动同步)
→ 新节点加入时,自动同步历史数据

面试加分回答

“RabbitMQ 的镜像队列有个坑:如果 ha-sync-mode=manual(手动同步),新 Slave 加入时不会自动同步历史数据,导致新 Slave 提升为 Master 后数据不全。我们曾经踩过这个坑——解决方法是设置 ha-sync-mode: automatic,并且用 ha-promote-on-shutdown: always 确保优雅关闭时主动同步数据给 Slave。”


第 35 题:消息队列选型:RocketMQ vs Kafka vs RabbitMQ,怎么选?

一句话总结日志/流处理用 Kafka订单/事务用 RockerMQ低延迟小项目用 RabbitMQ

深度解析

维度 Kafka RockerMQ RabbitMQ
吞吐量 10万级 QPS 10万级 QPS 万级 QPS
延迟 ms 级 ms 级 μs 级(最低)
顺序性 Partition 内有序 队列内有序 队列内有序
事务消息 0.11+ 支持 原生支持(最完善) 不支持
延迟消息 不支持(需自己实现) 原生支持(18个延迟级别) 原生支持(TTL+死信队列)
适用场景 日志、流处理、大数据管道 订单、支付、事务最终一致性 小项目、低延迟、灵活路由

面试加分回答

“我们公司同时用 Kafka 和 RockerMQ:① 日志采集(Logback → Kafka → ElasticSearch)用 Kafka,因为吞吐量高,而且 Kafka 的 Partition 机制方便并行消费 ② 订单系统用 RockerMQ,因为需要事务消息(订单创建 + 库存扣减 + 优惠券核销必须原子性)③ 内部通知系统(如新用户注册发邮件)用 RabbitMQ,因为消息量不大,但要求低延迟(RabbitMQ 的延迟是微秒级,比 Kafka/RocketMQ 快 10 倍)。”


消息队列 35 题完结! 建议把 31~35 题也过一遍,这些是大厂(字节/阿里/美团)高频题。


第 36 题:RocketMQ 的延迟消息(定时消息)原理?

一句话结论

RocketMQ 延迟消息:消息发送后,不会立即被消费,而是等待指定的延迟时间后才投递给消费者。实现原理是定时线程扫描 + 延迟队列


深度解析

RocketMQ 延迟消息的使用

1
2
3
4
5
// 发送延迟消息(延迟 5 分钟)
Message message = new Message("TopicTest", "Hello".getBytes());
// 设置延迟级别(1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h)
message.setDelayTimeLevel(8); // 延迟 5 分钟
producer.send(message);

RocketMQ 延迟消息的实现原理

1
2
3
4
5
6
7
8
9
10
11
1. 生产者发送延迟消息(设置 delayTimeLevel)

2. Broker 收到消息后,把消息放到**延迟队列**(Schedule_Topic)
→ 延迟队列有多个(对应不同的延迟级别)
→ 如:Schedule_Topic_1(1s)、Schedule_Topic_2(5s)、Schedule_Topic_8(5m)

3. 定时线程(Timer)扫描延迟队列
→ 到了投递时间,把消息从延迟队列取出来
→ 放到原始的 Topic(业务 Topic)

4. 消费者从原始 Topic 拉取消息(此时延迟时间已到)

延迟消息的应用场景

场景 说明
订单超时关闭 订单创建后,发送延迟消息(30 分钟),时间到了关闭订单
重试通知 通知失败后,延迟 5 分钟重试
定时任务 替换定时任务(避免定时任务扫表)

面试加分回答

RocketMQ 的延迟消息是基于定时扫描实现的,不是基于时间戳(所以延迟级别是固定的,不能自定义任意时间)。如果需要任意时间延迟(如”2026 年 12 月 31 日 23:59 投递”),可以用时间轮(Timing Wheel) 自己实现,或者用 XXL-Job 等分布式任务调度框架。


第 37 题:Kafka 的分区副本(Replica)同步机制?

一句话结论

Kafka 分区副本同步:Leader 副本负责读写,Follower 副本从 Leader 拉取数据同步;ISR(In-Sync Replicas) 列表维护”跟得上 Leader 的 Follower”,只有 ISR 中的 Follower 才能成为新的 Leader。


深度解析

分区副本的角色

1
2
3
4
5
6
7
一个分区(Partition)有多个副本(Replica):
1 个 Leader(负责读写)
→ N-1 个 Follower(只同步数据,不对外服务)

生产者 → Leader 副本(写入)

Follower 副本(主动拉取数据同步)

ISR(In-Sync Replicas)机制

1
2
3
4
5
6
7
8
ISR:跟得上 Leader 的 Follower 列表
→ Follower 的 LEO(Log End Offset)和 Leader 的 LEO 相差不超过 `replica.lag.time.max.ms`(默认 30 秒)
→ 如果 Follower 同步太慢,会被踢出 ISR(等它追上来了再加回来)

ACK 级别:
acks=0:不等待副本确认(最快,但可能丢数据)
acks=1:只等 Leader 确认(默认,Leader 挂了可能丢数据)
acks=all:等所有 ISR 副本确认(最慢,但不丢数据)

Leader 挂了,怎么选举新 Leader?

1
2
3
4
5
6
1. Controller 检测到 Leader 挂了

2. 从 ISR 列表中选择第一个 Follower 作为新 Leader
→ 如果 ISR 为空(所有 Follower 都同步太慢),看 `unclean.leader.election.enable`:
false(默认):等待 ISR 恢复(可用性降低,但不丢数据)
true:从非 ISR 的 Follower 中选一个(可能丢数据,但可用性高)

面试加分回答

Kafka 的 ISR 机制是可用性和一致性的平衡。实际项目中,acks=all + min.insync.replicas=2 可以保证”写入成功后,至少 2 个副本有数据”(即使 Leader 挂了,数据也不会丢)。但要注意:acks=all 会显著增加延迟(要等所有 ISR 副本确认),所以如果对延迟敏感,可以用 acks=1(允许 Leader 挂了丢少量数据)。


第 38 题:RabbitMQ 的死信队列(Dead Letter Queue)原理?

一句话结论

死信队列(DLQ):当消息变成死信(被拒绝、过期、队列满了)时,RabbitMQ 会把死信重新投递到死信交换机(DLX),再由 DLX 路由到死信队列,供人工排查。


深度解析

什么情况下消息会变成死信?

情况 说明
消息被拒绝(basic.reject/basic.nack)且不重新入队 消费者主动拒绝消息
消息 TTL 过期 消息在队列中停留超过 TTL
队列达到最大长度 队列满了,最早的消息变成死信

如何配置死信队列?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 声明死信交换机和死信队列
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange("dead.letter.exchange");
}

@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable("dead.letter.queue").build();
}

// 2. 业务队列绑定死信交换机
@Bean
public Queue businessQueue() {
return QueueBuilder.durable("business.queue")
.withArgument("x-dead-letter-exchange", "dead.letter.exchange") // 死信投递到这个交换机
.withArgument("x-dead-letter-routing-key", "dead.letter.key") // 死信的 Routing Key
.withArgument("x-message-ttl", 60000) // 消息 TTL(60 秒)
.build();
}

死信队列的应用场景

场景 说明
消息消费失败 重试 N 次后,拒绝消息 → 进入死信队列(人工排查)
消息 TTL 过期 订单 30 分钟未支付 → 进入死信队列(关闭订单)
队列满了 新消息无法入队 → 变成死信(避免阻塞生产者)

面试加分回答

死信队列是消息补偿机制的核心。实际项目中,如果消息消费失败,不要直接丢弃(会丢数据),而是重试 3~5 次后进入死信队列,然后有专门的”死信消费者”处理死信(如记录日志、发告警、人工介入)。另外,RocketMQ 也有类似的机制(重试队列 + 死信队列),原理类似。


第 39 题:消息队列的 TPS 和 QPS 怎么计算?

一句话结论

TPS(Transactions Per Second):每秒事务数(生产者发送 + 消费者消费);QPS(Queries Per Second):每秒查询数(对消息队列来说 = TPS)。计算方式:压测工具(如 JMeter)消息队列自带指标(如 Kafka 的 Records/s)。


深度解析

TPS vs QPS

指标 说明 适用场景
TPS 每秒事务数 数据库、消息队列
QPS 每秒查询数 HTTP 服务、读多写少的场景
对消息队列 QPS ≈ TPS 发送消息 = 1 个事务

如何压测消息队列的 TPS?

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 生产者压测(测试发送 TPS)
long start = System.currentTimeMillis();
int count = 1000000;
for (int i = 0; i < count; i++) {
producer.send(new Message("TopicTest", ("Message-" + i).getBytes()));
}
long end = System.currentTimeMillis();
double tps = count * 1000.0 / (end - start);
System.out.println("发送 TPS:" + tps);

// 2. 消费者压测(测试消费 TPS)
// 用线程池并发消费,统计每秒消费的消息数

Kafka 的 TPS 指标(自带)

1
2
3
4
5
6
7
8
9
# 查看 Kafka 的 TPS(Bytes/s、Records/s)
kafka-run-class.sh kafka.tools.ConsumerOffsetChecker \
--zookeeper localhost:2181 \
--group test-group

# 输出:
# Topic Pid Offset logSize Lag Owner
# TopicTest 0 12345 12345 0 consumer-1
# → Records/s = (当前 Offset - 上次 Offset) / 时间间隔

面试加分回答

消息队列的 TPS 是选型的重要指标。实际项目中,如果业务需要 10 万 TPS,但 Kafka 单机只能达到 5 万 TPS,就需要扩容 Broker(加机器)。另外,TPS 和分区数有很大关系:分区数越多,并行度越高,TPS 越高(但分区数太多会增加 Broker 的负载,要权衡)。


第 40 题:消息队列的消息堆积(Lag)怎么监控和告警?

一句话结论

消息堆积(Lag):消费者消费速度赶不上生产者发送速度,导致消息在 Broker 积压。监控方式:JMX 指标(Kafka)、控制台(RocketMQ)、自定义监控脚本;告警方式:企业微信/钉钉/短信


深度解析

Kafka 的消息堆积监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 用 kafka-consumer-groups.sh 查看 Lag
kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--group test-group \
--describe

# 输出:
# GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# test-group TopicTest 0 1000 5000 4000
# → Lag = LOG-END-OFFSET - CURRENT-OFFSET = 4000(堆积了 4000 条)

# 2. 用 JMX 指标监控(推荐)
# Kafka 暴露了 JMX 指标:
# - kafka.consumer:type=consumer-fetch-manager-metrics,client-id=*
# → records-lag-max(最大 Lag)
# → records-consumed-rate(消费速率)

RocketMQ 的消息堆积监控

1
2
3
4
5
6
7
8
9
# 1. 用 mqadmin 查看 Lag
mqadmin consumerProgress -g test-group

# 输出:
# #Topic #Broker Offset #Consumer Offset #Diff(堆积量)
# TopicTest 5000 1000 4000

# 2. 用 RocketMQ 控制台查看(Web UI)
# http://localhost:8080/consumer/list

告警规则

级别 Lag 阈值 动作
提示 Lag > 1000 记录日志
警告 Lag > 10000 企业微信/钉钉通知
严重 Lag > 100000 电话/短信告警(立即介入)

面试加分回答

消息堆积是生产环境的高频问题。除了监控 Lag,还要监控消费速率(records-consumed-rate)和消费延迟(end-to-end latency)。如果 Lag 一直在增长,说明消费速率 < 生产速率,需要扩容消费者(加机器)或优化消费逻辑(如批量消费、异步处理)。另外,告警阈值要根据业务设置(如”订单支付” Topic 的 Lag 不能超过 100,否则用户支付后很久才到账)。


第 41 题:RocketMQ 的 NameServer 和 Broker 的关系?

一句话结论

NameServer:路由注册中心(类似 Kafka 的 ZooKeeper,但更轻量),管理 Broker 的路由信息;Broker:消息存储和传输的节点,负责消息的收发、存储、拉取。


深度解析

NameServer 的作用

1
2
3
4
NameServer = 路由注册中心
→ Broker 启动时,向所有 NameServer 注册自己的路由信息(Topic → Broker 地址)
→ 生产者/消费者从 NameServer 拉取路由信息(知道了"Topic 在哪些 Broker 上"
→ NameServer 之间不通信(彼此独立,Broker 向所有 NameServer 注册)

Broker 的作用

1
2
3
4
Broker = 消息存储和传输节点
→ 接收生产者发送的消息,写入 CommitLog
→ 接收消费者拉取消息的请求,从 CommitLog 读取消息
→ 主从同步(Master Broker 同步数据给 Slave Broker)

NameServer vs ZooKeeper(Kafka 用 ZooKeeper)

对比项 NameServer(RocketMQ) ZooKeeper(Kafka)
部署复杂度 简单(无状态,彼此不通信) 复杂(需要集群部署,维护 ZAB 协议)
可用性 高(NameServer 挂了,Broker 仍然可以提供服务) 依赖 ZooKeeper(ZooKeeper 挂了,Kafka 不可用)
功能 只做路由注册和发现 路由注册 + 集群元数据管理 + Controller 选举

面试加分回答

NameServer 是 RockerMQ 的亮点之一(比 Kafka 的 ZooKeeper 轻量很多)。实际部署中,NameServer 通常部署 2 个节点(互不通信,Broker 向两个 NameServer 都注册),即使一个 NameServer 挂了,另一个仍然可以提供路由发现服务。另外,RocketMQ 5.0+ 正在去 NameServer 化(用 Proxy 模式 取代 NameServer),简化部署架构。


第 42 题:Kafka 的日志压缩(Log Compaction)是什么?

一句话结论

Log Compaction(日志压缩):Kafka 会保留每个 Key 的最新 Value,删除旧 Value(类似 Java 的 HashMap.put(key, value) 覆盖旧值)。适用场景:变更日志、状态快照


深度解析

为什么需要 Log Compaction?

1
2
3
4
5
6
7
8
场景:用 Kafka 存储"用户状态变更日志"
→ 用户 1001 的状态变更:
Key=1001, Value={"status": "CREATED"} (偏移量 0)
Key=1001, Value={"status": "PAID"} (偏移量 10)
Key=1001, Value={"status": "SHIPPED"} (偏移量 20)
→ 如果做了 Log Compaction:
→ 只保留 Key=1001 的最新 Value({"status": "SHIPPED"})
→ 删除旧 Value(节省磁盘空间)

Log Compaction vs Log Deletion

策略 说明 适用场景
Log Deletion(默认) 按时间/大小删除旧日志段 日志、事件流(不关心 Key)
Log Compaction 保留每个 Key 的最新 Value 变更日志、状态快照(关心 Key 的最新状态)

如何开启 Log Compaction?

1
2
3
4
5
6
# 创建 Topic 时指定清理策略
kafka-topics.sh \
--create \
--topic user-status \
--config cleanup.policy=compact \
--bootstrap-server localhost:9092

面试加分回答

Log Compaction 是 Kafka 的高级特性,它让 Kafka 不仅能做”消息队列”,还能做”KV 存储”(类似 RocksDB)。实际项目中,如果你需要存储每个用户的最新状态(如”用户是否已注销”),可以用 Log Compaction Topic(Kafka Streams 的 KTable 就是基于 Log Compaction 实现的)。另外,Log Compaction 不会删除”当前活跃的日志段”,所以磁盘空间的释放是滞后的(不是立即生效)。


第 43 题:消息队列常见坑(使用注意事项)?

一句话结论

消息队列的 8 个大坑:① 消息丢失;② 重复消费;③ 消息堆积;④ 顺序错乱;⑤ 消费超时;⑥ 消息过大;⑦ 消费者挂了;⑧ 生产者限流。


深度解析

坑 1:消息丢失

1
2
3
4
5
6
7
8
9
原因:
① 生产者:发送失败没重试
② Broker:刷盘前宕机(没配置同步刷盘)
③ 消费者:消费到消息,但业务处理失败(没做幂等)

解决:
① 生产者:开启 ACks=all + 重试
② Broker:配置同步刷盘(flushDiskType=SYNC_FLUSH)
③ 消费者:手动提交 Offset + 业务幂等

坑 2:重复消费

1
2
3
4
5
6
7
原因:
① 生产者:网络超时,重试发送(Broker 其实收到了)
② 消费者:消费成功,但提交 Offset 前挂了(重启后重新消费)

解决:
① 生产者:开启幂等(enable.idempotence=true
② 消费者:业务幂等(数据库唯一索引 / Redis SETNX)

坑 3:消息堆积

1
2
3
4
5
6
7
原因:
① 消费者太少(消费能力 < 生产能力)
② 消费逻辑太慢(如同步调用外部接口)

解决:
① 扩容消费者(加机器 / 加线程)
② 优化消费逻辑(批量消费、异步处理)

面试加分回答

消息队列的坑非常多,实际项目中至少要保证:① 消息不丢失(ACks + 刷盘策略 + 手动提交 Offset);② 消费幂等(数据库唯一索引);③ 监控 Lag(堆积了立即告警)。如果能说出这 3 点,面试已经及格了。另外,不要为了用消息队列而用消息队列——如果业务不需要”解耦、异步、削峰”,直接同步调用反而更简单。


第 44 题:消息队列的最佳实践?

一句话结论

消息队列最佳实践:① 生产者开启重试 + 幂等;② 消息设置 TTL(避免无限堆积);③ 消费者手动提交 Offset + 幂等;④ 监控 Lag + 告警;⑤ 重要 Topic 配置多副本。


深度解析

最佳实践清单

实践 说明
生产者开启重试 retries=3(网络抖动时自动重试)
生产者开启幂等 enable.idempotence=true(避免网络超时重试导致重复写入)
消息设置 TTL message.setDelayTimeLevel(8)(避免消息无限堆积)
消费者手动提交 Offset enable.auto.commit=false(消费成功后再提交)
消费者幂等 数据库唯一索引 / Redis SETNX(避免重复消费)
监控 Lag kafka-consumer-groups.sh --describe(堆积了立即告警)
重要 Topic 配置多副本 replication.factor=3(即使一个 Broker 挂了,数据不丢)

消息队列的线程模型(消费者)

1
2
3
4
5
6
7
8
9
// Kafka 消费者(单线程拉取,多线程消费)
@Bean
public KafkaListenerContainerFactory kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory factory =
new ConcurrentKafkaListenerContainerFactory();
// 并发度 = 分区数(一个分区只能被一个消费者线程消费)
factory.setConcurrency(3); // 3 个消费者线程
return factory;
}

面试加分回答

消息队列的最佳实践是”生产环境不踩坑”的保证。实际项目中,我建议用Spring Kafka / Spring RocketMQ(封装好的 Starter),而不是自己写原生客户端——Starter 已经帮你配置了重试、幂等、手动提交 Offset 等最佳实践,不容易出错。另外,消息队列不适合做”强一致性”场景(如金融转账),这种场景还是要用 2PC / TCC 分布式事务


第 45 题:消息队列的未来趋势(Pulsar、边缘 MQ)?

一句话结论

消息队列的未来趋势:① Pulsar(下一代消息队列,存算分离);② 边缘 MQ(IoT 场景,消息队列跑到边缘节点);③ Serverless MQ(按调用次数计费,不用预留资源)。


深度解析

Pulsar vs Kafka

对比项 Kafka Pulsar
架构 存储和计算耦合(Broker 同时负责存储和计算) 存储和计算分离(Broker 负责计算,BookKeeper 负责存储)
多租户 不支持(需要手动隔离) 原生支持(Namespace 级别隔离)
跨地域复制 需要 MirrorMaker 原生支持(Geo-Replication)
消费模型 只有 Pull Pull + Push(支持 Queue 模型)

边缘 MQ(IoT 场景)

1
2
3
场景:10 万个 IoT 设备(如智能电表)上报数据
→ 问题:设备在网络边缘(如偏远山区),连不上中心 Kafka
→ 解决:边缘 MQ(如 EMQX)→ 在边缘节点缓存消息,网络恢复后再同步到中心

Serverless MQ(按调用次数计费)

1
2
3
4
传统 MQ:按 Broker 节点数计费(不管你用不用,都要付钱)
Serverless MQ:按 API 调用次数计费(不用不付钱)
→ 代表:AWS SQS、Azure Service Bus
→ 适合:流量波动大的场景(如电商大促)

面试加分回答

Pulsar 是下一代消息队列的有力竞争者(Yahoo 开源,现在已经 Apache 顶级项目)。它的存算分离架构比 Kafka 更适合云原生场景(Broker 可以无缝扩容,存储层 BookKeeper 也支持无缝扩容)。但 Kafka 的生态更成熟(连接器多、文档多、社区大),所以目前(2026 年)还是 Kafka 的天下。如果是新项目,可以考虑 Pulsar(未来可能取代 Kafka)。


第 46 题:如何保证消息队列的高可用(深入)?

一句话结论

消息队列高可用:Broker 多副本(Leader + Follower)+ 消费者集群(多个消费者订阅同一个 Topic)+ 监控告警(Lag、TPS、节点健康状态)+ 降级预案(消息队列挂了,降级为同步调用)。


深度解析

Broker 高可用

1
2
3
4
5
6
7
8
9
Kafka:
→ 多副本(replication.factor=3)
acks=all(所有 ISR 副本确认才返回成功)
→ min.insync.replicas=2(至少 2 个 ISR 副本,否则拒绝写入)

RocketMQ:
→ 主从同步(Master Broker + Slave Broker)
→ 同步刷盘(flushDiskType=SYNC_FLUSH)
→ Dledger 模式(RocketMQ 5.0+,基于 Raft 协议,自动选主)

消费者高可用

1
2
3
4
消费者集群:
→ 多个消费者订阅同一个 Topic(同一个 Consumer Group)
→ 一个消费者挂了,其他消费者继续消费(不会停止消费)
→ Kafka:分区数 >= 消费者数(一个分区只能被一个消费者线程消费)

降级预案

1
2
3
4
场景:消息队列挂了(如 Kafka 集群宕机)
→ 降级方案:
① 生产者:降级为同步调用(直接调用消费者接口)
② 消费者:降级为轮询数据库(定时任务扫表,代替消息驱动)

面试加分回答

消息队列的高可用是生产环境的必答题。实际项目中,除了 Broker 多副本、消费者集群,还要做混沌工程(Chaos Engineering)——主动杀掉 Broker 节点、断网、杀消费者进程,看系统是否能自动恢复。另外,监控告警是高可用的”眼睛”——如果没有监控,Broker 挂了都不知道,高可用就是一句空话。


第 47 题:消息队列的事务消息(分布式事务)?

一句话结论

事务消息:保证”本地事务”和”消息发送”的原子性(要么都成功,要么都失败)。RocketMQ 事务消息的实现原理:两阶段提交(2PC) + 事务状态回查


深度解析

事务消息的使用场景

1
2
3
场景:订单服务创建订单后,发送"订单创建"消息给库存服务
→ 问题:如果订单数据库事务提交成功,但消息发送失败 → 库存服务不知道有新订单(数据不一致)
→ 解决:事务消息(订单数据库事务和消息发送绑定在一起)

RocketMQ 事务消息的流程

1
2
3
4
5
6
7
8
9
10
11
12
1. 生产者发送"半消息"(Half Message)给 Broker
→ 半消息:对消费者不可见(Broker 不会投递给消费者)

2. 生产者执行本地事务(如"创建订单"数据库事务)

3. 生产者根据本地事务的结果,向 Broker 提交"提交""回滚"
→ 提交:Broker 把半消息标记为"可投递"(消费者可以消费了)
→ 回滚:Broker 删除半消息(消费者不会看到这条消息)

4. 如果生产者挂了(没提交也没回滚)
Broker 会定时回查生产者的"事务状态"(-checkLocalTransaction)
→ 生产者根据本地事务的结果,返回"提交""回滚"

面试加分回答

RocketMQ 事务消息是分布式事务的轻量级解决方案(不用的 2PC / TCC 那么重)。但要注意:事务消息只保证”生产者本地事务和消息发送”的原子性,不保证”消费者消费成功”(消费者消费失败,需要业务幂等 + 重试)。另外,Kafka 没有原生的事务消息(需要用 Transactional API,但只保证”消息发送”的原子性,不保证本地事务)。


第 48 题:消息队列的消息轨迹(Message Tracing)最佳实践?

一句话结论

消息轨迹:追踪一条消息从”生产 → Broker → 消费”的全链路状态。实现方式:消息 ID + 分布式链路追踪(SkyWalking / Pinpoint)


深度解析

消息轨迹的实现方案

方案 说明 适用场景
方案 A:消息 ID + DB 发送时写入 DB(状态=SENT),消费后更新 DB(状态=CONSUMED) 简单场景(消息量不大)
方案 B:分布式链路追踪 消息里带入 traceId,和 HTTP 请求打通 复杂场景(微服务链路长)
方案 C:消息队列自带的轨迹功能 RocketMQ 有消息轨迹功能(默认关闭) RocketMQ 用户

RocketMQ 消息轨迹的配置

1
2
3
4
5
6
7
8
9
10
11
// 1. Broker 开启消息轨迹(broker.conf)
traceTopicEnable=true

// 2. 生产者开启消息轨迹
DefaultMQProducer producer = new DefaultMQProducer("group", true); // true = 开启消息轨迹

// 3. 消费者开启消息轨迹
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group", true); // true = 开启消息轨迹

// 4. 查看消息轨迹(RocketMQ 控制台)
// http://localhost:8080/message/trace

面试加分回答

消息轨迹是生产环境排查问题的利器。实际项目中,我建议用分布式链路追踪(SkyWalking / Pinpoint)——不仅可以看到消息的轨迹,还可以看到”HTTP 请求 → 发送消息 → 消费消息 → 写数据库”的完整链路,排查问题非常方便。另外,消息轨迹会产生额外的存储开销(RocketMQ 会创建 RMQ_SYS_TRACE_TOPIC 来存储轨迹数据),所以要定期清理。


第 49 题:消息队列的配额管理(Quota Management)怎么做?

一句话结论

配额管理:限制生产者/消费者的 TPS、消息大小、Topic 数量,避免”一个租户用光所有资源”的问题。实现方式:Broker 端限流(Kafka 的 Quota)、客户端限流(Guava RateLimiter)。


深度解析

Kafka 的 Quota 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 限制生产者的 TPS(每秒最多写入 10 MB)
kafka-configs.sh \
--alter \
--add-config producer_byte_rate=10485760 \
--entity-type clients \
--entity-name producer-group \
--bootstrap-server localhost:9092

# 2. 限制消费者的 TPS(每秒最多读取 20 MB)
kafka-configs.sh \
--alter \
--add-config consumer_byte_rate=20971520 \
--entity-type clients \
--entity-name consumer-group \
--bootstrap-server localhost:9092

RocketMQ 的流控配置

1
2
3
4
5
// Broker.conf
# 限制生产者 TPS(每秒最多 1000 条)
sendMessageThreadPoolNums=16
# 限制消费者 TPS(每秒最多 2000 条)
pullMessageThreadPoolNums=16

客户端限流(Guava RateLimiter)

1
2
3
4
5
6
7
// 生产者限流(每秒最多发送 1000 条)
RateLimiter rateLimiter = RateLimiter.create(1000.0);

public void send(Message message) {
rateLimiter.acquire(); // 限流(如果超过 1000 TPS,阻塞等待)
producer.send(message);
}

面试加分回答

配额管理是多租户消息队列的必备功能。实际项目中,如果是私有部署(公司内部使用),可以不配置配额(大家都是同事,不会故意搞破坏);但如果是公有云(如阿里云 RocketMQ、AWS MSK),必须配置配额(不同租户的 Topic 要隔离,避免相互影响)。另外,Kafka 的 Quota 是基于client.id 的(不是基于 Topic),所以如果要限制某个 Topic 的 TPS,需要在客户端配置 client.id=TopicName-producer


第 50 题:消息队列的压测(Benchmark)怎么做?

一句话结论

消息队列压测:用压测工具(如 JMeter、Kafka 自带的 kafka-producer-perf-test.sh)模拟高并发,测试 TPS、延迟、吞吐量。核心指标:TPS(每秒消息数)延迟(P99、P999)磁盘 I/O 使用率


深度解析

Kafka 自带的压测工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 生产者压测(测试发送 TPS)
kafka-producer-perf-test.sh \
--topic TopicTest \
--num-records 1000000 \
--record-size 1024 \
--throughput -1 \
--producer-props bootstrap.servers=localhost:9092

# 输出:
# 1000000 records sent, 50000 records/sec (48.83 MB/sec), 200.0 ms avg latency, 500.0 ms max latency

# 2. 消费者压测(测试消费 TPS)
kafka-consumer-perf-test.sh \
--topic TopicTest \
--messages 1000000 \
--bootstrap-server localhost:9092

# 输出:
# data.consumed: 1024 MB, MB.sec: 50.00, records.sec: 50000.0

压测的核心指标

指标 说明 目标
TPS 每秒消息数 越高越好(如 10 万 TPS)
延迟(P99) 99% 的消息延迟小于多少 越低越好(如 P99 < 10ms)
磁盘 I/O 使用率 Broker 的磁盘 I/O 是否达到瓶颈 < 80%(避免影响其他进程)
网络带宽使用率 Broker 的网络带宽是否达到瓶颈 < 80%

面试加分回答

消息队列的压测是选型的重要依据。实际项目中,压测要在生产环境的硬件配置上做(不要用笔记本压测,结果不准)。另外,压测时要模拟真实场景(消息大小、分区数、副本数都要和生产环境一致),否则压测结果没有参考价值。我们之前选型时,用 3 台 8C 16G 的机器压测 Kafka 和 RocketMQ,Kafka 的 TPS 是 RocketMQ 的 2 倍,所以最终选了 Kafka(我们的场景是日志采集,需要高 TPS)。



消息队列面试八股文(深度版)|RabbitMQ + Kafka 30题讲透
https://whyalwaysme.lol/2026/06/08/2026-06-08-mq-interview-deep/
作者
Cassiur
发布于
2026年6月8日
许可协议