消息队列中间件面试题 —— 从 Kafka 架构到消息可靠性的深度问答
覆盖Kafka架构(分区/副本/ISR/零拷贝/消费者组)、RabbitMQ(交换机/AMQP/死信队列)、RocketMQ事务消息、消息顺序性/幂等性/积压处理、MQ选型对比,25 道高频题附架构图
消息队列是后端系统的核心中间件——几乎所有分布式系统都依赖 MQ 做解耦、异步和削峰。面试中能讲清 Kafka 的零拷贝原理、消息丢失的全链路防护、顺序消息的实现方案,展示的是对分布式系统的实战理解。
这篇文章从核心概念 → Kafka 深入 → RabbitMQ → RocketMQ → 通用难题五条线展开,每道题都带架构图和方案对比。
📌 关联阅读:系统设计面试题 · Redis 与缓存架构面试题 · 分布式理论面试题
第一部分:消息队列核心概念
Q1:为什么要用消息队列?核心作用是什么?
记忆点:解耦、异步、削峰三板斧
1
2
3
4
5
6
7
8
9
10
11
同步调用(无MQ):
用户下单 → 扣库存 → 扣积分 → 发短信 → 返回成功
总耗时 = 50+50+50+50 = 200ms
任一服务挂 → 整个下单失败
异步解耦(有MQ):
用户下单 → 发消息到MQ → 返回成功(50ms)
↓
┌─────────┼──────────┐
↓ ↓ ↓
扣库存 扣积分 发短信 ← 异步消费,互不影响
| 作用 | 说明 | 示例 |
|---|---|---|
| 解耦 | 上游不关心下游有多少消费者 | 订单服务不依赖短信服务 |
| 异步 | 非核心流程异步处理 | 下单后异步发通知 |
| 削峰 | 突发流量先进 MQ,消费者匀速处理 | 秒杀请求进队列 |
| 广播 | 一条消息多个消费者处理 | 数据变更通知多个系统 |
| 最终一致性 | 配合重试实现跨服务事务 | 分布式事务 |
引入 MQ 的代价:系统复杂度增加、消息可靠性保障、消息顺序问题、消息积压风险。
Q2:Kafka、RabbitMQ、RocketMQ 怎么选?
记忆点:Kafka 大数据流,RabbitMQ 企业可靠,RocketMQ 电商事务
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 定位 | 分布式流处理平台 | 企业级消息中间件 | 分布式消息/事务 |
| 吞吐量 | 百万级/秒 | 万级/秒 | 十万级/秒 |
| 延迟 | ms 级 | μs 级(最低) | ms 级 |
| 消息模型 | 拉模型(Pull) | 推模型(Push) | 拉模型(Pull) |
| 消息回溯 | ✅(按 offset) | ❌ | ✅(按时间戳) |
| 事务消息 | 有(不常用) | ❌ | ✅(半消息+回查) |
| 延迟消息 | ❌(需插件) | ✅(TTL+DLX) | ✅(18个等级) |
| 消息查询 | 按 offset | ❌ | ✅(按 msgId/key) |
| 协议 | 自定义 | AMQP | 自定义 |
| 语言 | Scala/Java | Erlang | Java |
| 典型用户 | LinkedIn/Uber | 银行/电信 | 阿里/滴滴 |
选型建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 日志采集/大数据 | Kafka | 吞吐量最高 |
| 实时流处理 | Kafka | Kafka Streams/Flink 生态 |
| 企业级可靠消息 | RabbitMQ | 协议完善、路由灵活 |
| 低延迟业务消息 | RabbitMQ | μs 级延迟 |
| 电商交易/事务 | RocketMQ | 事务消息原生支持 |
| 延迟消息/定时任务 | RocketMQ | 延迟消息原生支持 |
第二部分:Kafka 深入
Q3:Kafka 的整体架构是什么?
记忆点:Topic → Partition → Replica → Segment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kafka 集群架构:
Producer ──→ ┌──────────────────────────────┐
│ Kafka Cluster │
│ ┌──────────┐ ┌──────────┐ │
│ │ Broker 0 │ │ Broker 1 │ │
│ │ P0(L) │ │ P0(F) │ │
│ │ P1(F) │ │ P1(L) │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ Broker 2 │ L=Leader │
│ │ P0(F) │ F=Follower │
│ │ P1(F) │ │
│ └──────────┘ │
└──────────────────────────────┘
↓
Consumer Group ──→ Consumer1(P0), Consumer2(P1)
Topic "orders" 有 2 个 Partition,3 个副本(replication-factor=3)
核心概念速查:
| 概念 | 说明 |
|---|---|
| Broker | Kafka 服务器节点 |
| Topic | 逻辑消息分类 |
| Partition | Topic 的分片,是并行度的基本单位 |
| Replica | Partition 的副本(1 Leader + N Follower) |
| ISR | In-Sync Replicas,与 Leader 保持同步的副本集合 |
| Offset | 消息在 Partition 中的唯一偏移量 |
| Consumer Group | 消费者组,组内每个 Partition 只被一个消费者消费 |
Q4:Kafka 为什么吞吐量这么高?
记忆点:顺序写 + 页缓存 + 零拷贝 + 批量 + 分区并行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
五大性能优化:
1. 顺序写磁盘(追加写入,不随机写)
随机写 HDD: ~100 IOPS
顺序写 HDD: ~600 MB/s ← 快 6000 倍!
2. 页缓存(Page Cache)
写入不直接落盘,先写 OS 页缓存
→ 写入速度 ≈ 内存速度
→ 消费者读取热数据也命中页缓存
3. 零拷贝(sendfile)
传统:磁盘 → 内核缓冲 → 用户缓冲 → Socket缓冲 → 网卡
零拷贝:磁盘 → 内核缓冲 ──────────────────→ 网卡
→ 消费者拉取时使用 sendfile,减少 2 次数据拷贝
4. 批量发送 + 压缩
Producer 攒一批消息一起发(linger.ms + batch.size)
支持 gzip/snappy/lz4/zstd 压缩
5. 分区并行
多个 Partition → 多个消费者并行消费
→ 水平扩展能力
Q5:Kafka 的 ISR 机制是什么?消息什么时候算”已提交”?
记忆点:ISR = 和 Leader 保持同步的副本集合,只有 ISR 中的副本参与提交
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ISR 动态变化:
初始 ISR = {Leader, Follower1, Follower2}
Follower2 落后太多(replica.lag.time.max.ms 超时):
ISR = {Leader, Follower1} ← Follower2 被踢出
Follower2 追上 Leader:
ISR = {Leader, Follower1, Follower2} ← 重新加入
消息提交条件(acks 配置):
acks=0 Producer 不等确认 → 可能丢消息
acks=1 Leader 写入就确认 → Leader 挂了可能丢
acks=all 所有 ISR 副本写入才确认 → 最安全 ✅
min.insync.replicas=2 配合 acks=all
→ 至少 2 个 ISR 副本确认才算提交
→ 如果 ISR 只剩 1 个,拒绝写入(保证不丢)
Q6:Kafka 的消费者组(Consumer Group)是怎么工作的?
记忆点:组内分区独占,组间广播
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Topic: orders (3 Partitions)
Consumer Group A(3个消费者):
Consumer1 ← P0 每个消费者消费不同分区
Consumer2 ← P1 → 组内负载均衡
Consumer3 ← P2
Consumer Group B(2个消费者):
Consumer4 ← P0, P1 消费者少于分区数
Consumer5 ← P2 → 有的消费者消费多个分区
Consumer Group C(4个消费者):
Consumer6 ← P0
Consumer7 ← P1 消费者多于分区数
Consumer8 ← P2 → Consumer9 闲置!
Consumer9 ← (空闲)
关键规则:
- 同一 Group 内,一个 Partition 只能被一个 Consumer 消费
- 不同 Group 之间,消息会被广播(每个 Group 都能消费到)
- 消费者数量 > 分区数 → 多余消费者闲置
Rebalance(再均衡)触发条件:
- 消费者加入/退出 Group
- 订阅的 Topic 分区数变化
- 消费者被判定超时(session.timeout.ms)
Q7:Kafka 的消息存储结构是什么?
记忆点:Partition = 多个 Segment 文件,每个 Segment 有 .log + .index + .timeindex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Partition 目录结构:
topic-orders-0/
├── 00000000000000000000.log ← 消息数据(append-only)
├── 00000000000000000000.index ← 稀疏偏移量索引
├── 00000000000000000000.timeindex ← 时间戳索引
├── 00000000000005242880.log ← 新 Segment(基于大小/时间滚动)
├── 00000000000005242880.index
└── 00000000000005242880.timeindex
查找 offset=5242900 的消息:
1. 二分查找 Segment 文件(文件名就是起始 offset)
→ 定位到 00000000000005242880.log
2. 在 .index 中二分查找最近的索引条目
→ 找到 offset 5242888 → 物理位置 12345
3. 从物理位置 12345 开始顺序扫描到 offset 5242900
第三部分:RabbitMQ
Q8:RabbitMQ 的消息模型和 Exchange 类型?
记忆点:Producer → Exchange → Queue → Consumer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RabbitMQ 消息路由模型:
Producer → Exchange ─(routing_key)─→ Queue → Consumer
四种 Exchange 类型:
1. Direct:精确匹配 routing_key
Exchange ─ routing_key="order.created" ─→ Queue A
─ routing_key="order.paid" ─→ Queue B
2. Topic:通配符匹配
Exchange ─ "order.*" ─→ Queue A(匹配 order.xxx)
─ "order.#" ─→ Queue B(匹配 order.xxx.yyy...)
* = 匹配一个词, # = 匹配零或多个词
3. Fanout:广播(忽略 routing_key)
Exchange ─→ Queue A
─→ Queue B 所有绑定的队列都收到
─→ Queue C
4. Headers:根据消息头匹配(不常用)
Q9:RabbitMQ 怎么保证消息不丢失?
记忆点:三个环节都要保障:生产端 → Broker → 消费端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
消息丢失的三个环节及解决方案:
1. 生产端 → Broker(网络丢失)
解决:Publisher Confirm 机制
channel.confirmSelect();
channel.waitForConfirms(); // 同步等待 Broker 确认
// 或异步回调 addConfirmListener
2. Broker 持久化(宕机丢失)
解决:队列持久化 + 消息持久化
Queue: durable=true
Message: deliveryMode=2(持久化)
→ 消息写入磁盘后才确认
3. 消费端(处理前 ack 导致丢失)
解决:手动 ACK
autoAck=false
channel.basicAck(tag, false); // 处理完成后手动确认
→ 处理失败时 basicNack 重回队列
Q10:RabbitMQ 的死信队列(DLX)是什么?有什么用?
记忆点:死信 = 被拒绝/过期/队列满的消息,转发到死信交换机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
死信产生条件:
1. 消息被 basicNack/basicReject 且 requeue=false
2. 消息 TTL 过期
3. 队列达到最大长度
死信队列流程:
Normal Exchange → Normal Queue ──(死信)──→ DLX Exchange → DLX Queue
↑ ↑
消息过期/被拒 人工处理/重试
实际应用:
1. 延迟队列(TTL + DLX)
消息设 TTL=30min → 过期后进入 DLX → 消费者处理
→ 30 分钟后执行(如:订单超时取消)
2. 异常消息兜底
消费失败的消息进 DLX → 告警/人工排查
第四部分:RocketMQ
Q11:RocketMQ 的事务消息是怎么实现的?
记忆点:半消息 + 本地事务 + 回查机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RocketMQ 事务消息流程:
Producer Broker Consumer
│ │ │
│ 1.发送半消息(HALF) │ │
│ ─────────────────→ │ 存储半消息 │
│ ←──── 发送成功 ─────── │ (对消费者不可见) │
│ │ │
│ 2.执行本地事务 │ │
│ (扣款/下单等) │ │
│ │ │
│ 3.提交/回滚 │ │
│ COMMIT ──────────→ │ → 消息对消费者可见 │
│ 或 ROLLBACK ─────→ │ → 删除半消息 │
│ │ │
│ │ │
│ 如果 Producer 没响应 │ │
│ ←── 4.回查本地事务 ─── │ (定时回查) │
│ 返回 COMMIT/ROLLBACK → │ │
关键:即使 Producer 宕机,Broker 也能通过回查确认事务状态
对比两阶段提交:
| 维度 | 2PC | RocketMQ 事务消息 |
|---|---|---|
| 协调者 | 事务管理器 | MQ Broker |
| 阻塞 | 同步阻塞 | 异步非阻塞 |
| 可用性 | 协调者单点 | Broker 高可用 |
| 适用 | 强一致性 | 最终一致性 |
Q12:RocketMQ 的延迟消息怎么实现的?
记忆点:18 个延迟等级,用定时任务投递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
延迟等级(不支持任意时间):
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
实现原理:
1. Producer 发送延迟消息,指定 delayLevel=3(10s)
2. Broker 将消息存入特殊的 SCHEDULE_TOPIC(按等级分队列)
3. 定时任务每隔 1s 检查到期的消息
4. 到期 → 将消息投递到原始 Topic → 消费者可见
时间线:
T=0 Producer 发送 delayLevel=3(10s延迟)
T=0 Broker 存入 SCHEDULE_TOPIC_XXXX Queue3
T=10s 定时任务发现到期 → 投递到原始 Topic
T=10s Consumer 收到消息
第五部分:通用难题
Q13:如何保证消息不丢失?(全链路分析)
记忆点:生产端 + Broker + 消费端三环防护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
全链路消息不丢失方案:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Producer │ │ Broker │ │ Consumer │
│ │ │ │ │ │
│ 确认机制: │→ │ 持久化: │→ │ 手动ACK: │
│ Kafka:acks=all │ Kafka: │ │ 处理完再确认 │
│ RabbitMQ:confirm │ replication │ │ 失败重试 │
│ 重试+幂等 │ │ RabbitMQ: │ │ 死信队列兜底 │
│ │ │ durable+持久 │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
Kafka 具体配置:
Producer:
acks=all
retries=MAX_INT
enable.idempotence=true
Broker:
replication.factor >= 3
min.insync.replicas >= 2
unclean.leader.election.enable=false
Consumer:
enable.auto.commit=false(手动提交 offset)
Q14:如何保证消息不重复消费?(幂等性)
记忆点:MQ 不保证 exactly-once,靠消费端幂等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
消息重复的原因:
1. Producer 发送超时重试 → Broker 收到 2 条
2. Consumer 处理成功但 ack 失败 → rebalance 后重新消费
3. 网络波动导致重复投递
消费端幂等方案:
方案1:唯一 ID + 去重表
┌────────────────────────────────────────┐
│ 消费消息前: │
│ SELECT * FROM dedup WHERE msg_id=? │
│ 如果存在 → 跳过 │
│ 如果不存在 → 处理 + INSERT msg_id │
│ (在同一个事务中) │
└────────────────────────────────────────┘
方案2:数据库唯一约束
INSERT INTO orders (order_id, ...) VALUES (?, ...)
ON DUPLICATE KEY UPDATE ... (或忽略)
→ 天然幂等
方案3:Redis SET NX
if redis.setnx("consumed:" + msgId, 1, ex=86400):
process(msg)
else:
skip // 已消费过
方案4:乐观锁/版本号
UPDATE stock SET quantity=quantity-1
WHERE product_id=? AND version=?
→ 重复执行不会多扣
Q15:如何保证消息的顺序性?
记忆点:全局有序很难,通常只需分区有序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Kafka 顺序保证:
同一 Partition 内消息有序
→ 需要顺序的消息发到同一 Partition
// Producer: 按 key hash 到同一 partition
producer.send(new ProducerRecord("orders", orderId, message));
// 同一 orderId 的消息都进同一 partition → 有序
// Consumer: 单线程消费该 partition
// 如果多线程 → 用内存队列按 key 分发到固定线程
全局有序方案(性能很差,慎用):
Topic 只用 1 个 Partition + 1 个 Consumer
→ 完全有序,但吞吐量极低
RocketMQ 顺序消息:
// 发送时指定队列选择器
producer.send(msg, (queues, msg, arg) -> {
int id = (int) arg;
return queues.get(id % queues.size());
}, orderId);
实际场景:
订单创建 → 支付 → 发货(同一订单 ID 需要有序)
→ 按 orderId hash 到同一 Partition
→ 分区内单线程消费
Q16:消息积压了怎么办?
记忆点:先止血(扩容),再排查(找原因),最后优化(防复发)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
消息积压处理步骤:
1. 紧急止血(扩容消费者)
┌──────┐ ┌──────────────────────────┐
│ MQ │ → │ 临时扩容消费者实例 │
│积压100万│ │ Consumer × 10 │
└──────┘ │ (需要分区数 ≥ 消费者数) │
└──────────────────────────┘
如果分区数不够 → 临时创建新 Topic(更多分区)
用一个转发程序把积压消息搬到新 Topic
2. 排查根因
消费者挂了?→ 重启
消费太慢? → 优化消费逻辑(批量处理/异步)
上游暴增? → 限流/降级
3. 长期优化
消费者性能优化(批量消费、异步IO)
合理设置分区数(分区数 = 消费者数 × 2)
监控告警(消费延迟 > 阈值就告警)
消息 TTL(过期自动丢弃,适用于非关键消息)
Q17:如何实现延迟消息?各 MQ 的方案对比?
记忆点:各 MQ 支持程度不同,通用方案用时间轮
| MQ | 延迟消息支持 | 实现方式 |
|---|---|---|
| Kafka | ❌ 原生不支持 | 外部定时任务轮询 |
| RabbitMQ | ✅ TTL + DLX | 消息过期后进死信队列 |
| RocketMQ | ✅ 18个等级 | 内部定时任务投递 |
| Pulsar | ✅ 任意延迟 | 原生支持 |
通用方案:Redis + 时间轮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方案:Redis ZSet 做延迟队列
ZADD delay_queue <execute_timestamp> <message>
消费者循环:
messages = ZRANGEBYSCORE delay_queue 0 <now> LIMIT 0 100
for msg in messages:
if ZREM delay_queue msg: // 原子取出防重复
publish(msg) // 投递到正常队列
时间轮(Timing Wheel):
┌─────────────────────────────────────┐
│ [0] [1] [2] [3] [4] [5] [6] [7] │ ← 8个槽
│ ↑ │
│ 当前指针,每 tick 前进一格 │
│ 到达某槽 → 执行该槽的所有任务 │
└─────────────────────────────────────┘
多层时间轮可支持更长延迟(天/月级别)
第六部分:高可用与运维
Q18:Kafka 的 Leader 选举是怎么工作的?
记忆点:Controller 统一管理,ISR 中优先选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kafka Leader 选举机制:
1. Controller 选举(集群级别)
所有 Broker 竞争在 ZooKeeper 创建 /controller 节点
→ 第一个创建成功的成为 Controller
→ Controller 负责管理所有 Partition 的 Leader 选举
2. Partition Leader 选举
当某 Partition 的 Leader 挂了:
Controller 检测到(通过 ZooKeeper watch)
→ 从 ISR 列表中选第一个存活的副本作为新 Leader
→ 通知所有 Broker 更新元数据
ISR = [Broker1, Broker2, Broker3]
Broker1(Leader) 挂了 → Broker2 成为新 Leader
如果 ISR 为空?
unclean.leader.election.enable=true → 从非 ISR 中选(可能丢数据)
unclean.leader.election.enable=false → 该 Partition 不可用(推荐)
Q19:Kafka 如何实现 Exactly-Once 语义?
记忆点:幂等 Producer + 事务 = Exactly-Once
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
三种语义:
At-most-once: acks=0, 可能丢消息
At-least-once: acks=all + 重试, 可能重复
Exactly-once: 幂等 + 事务
1. 幂等 Producer(单分区去重)
enable.idempotence=true
→ 每条消息带 <PID, Sequence> 编号
→ Broker 检测到重复序号 → 丢弃
→ 只保证单分区内去重
2. 事务(跨分区原子写入)
producer.initTransactions();
producer.beginTransaction();
producer.send(record1); // 写入 Partition A
producer.send(record2); // 写入 Partition B
producer.commitTransaction(); // 原子提交
// 要么全部可见,要么全部不可见
Consumer 配置:
isolation.level=read_committed
→ 只读取已提交的事务消息
Q20:MQ 集群监控应该关注哪些指标?
记忆点:积压量 / 消费延迟 / 吞吐量 / 可用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
核心监控指标:
┌─────────────────────────────────────────────┐
│ 指标 │ 告警阈值 │ 含义 │
├─────────────────────────────────────────────┤
│ Consumer Lag │ > 10000 │ 消费积压│
│ 消费延迟(时间) │ > 5min │ 处理慢 │
│ 消息入队速率 │ 突增 3 倍 │ 流量暴增│
│ 消息出队速率 │ 骤降 50% │ 消费异常│
│ Broker 磁盘使用率 │ > 80% │ 存储告急│
│ ISR 收缩 │ ISR < replicas │ 副本落后│
│ Under-replicated │ > 0 │ 副本不足│
│ Request 延迟 │ p99 > 100ms │ 性能下降│
└─────────────────────────────────────────────┘
Kafka 常用监控命令:
# 查看消费者组 lag
kafka-consumer-groups.sh --describe --group mygroup
# 查看 Topic 分区信息
kafka-topics.sh --describe --topic orders
# 查看集群健康
kafka-metadata.sh --snapshot /path/to/metadata
第七部分:实战场景
Q21:秒杀系统中消息队列怎么用?
记忆点:前端限流 → MQ 削峰 → 后端匀速消费
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
秒杀架构:
用户请求(10万QPS)
↓ Nginx 限流
请求校验(库存预检/Redis)
↓ 通过的请求
写入 MQ(Kafka/RocketMQ)
↓ 消费者按固定速率处理
扣减库存(数据库)
↓
返回结果(异步通知/轮询)
关键设计:
1. 前端防刷:验证码 + 按钮灰置
2. 接入层限流:Nginx limit_req
3. Redis 预检:库存为 0 直接拒绝
4. MQ 削峰:高峰流量进队列
5. 下单服务:固定并发消费(如 100 QPS)
6. 数据库:乐观锁/分布式锁 防超卖
Q22:如何用消息队列实现分布式事务?
记忆点:本地消息表 / 事务消息两种主流方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
方案1:本地消息表
Service A (订单服务) MQ Service B (库存服务)
│ │ │
│ BEGIN TRANSACTION │ │
│ INSERT order │ │
│ INSERT message_table │ │
│ COMMIT │ │
│ │ │
│ 定时任务扫描 message_table │ │
│ ──── 发送消息 ────────→ │ │
│ │ ──── 消费 ────→ │
│ │ │ 扣减库存
│ │ ←── ACK ───── │
│ 标记消息已发送 │ │
方案2:RocketMQ 事务消息(见 Q11)
对比:
本地消息表:通用,任何 MQ 都行,但多了一张表
事务消息:RocketMQ 原生支持,更优雅
两者都是最终一致性
Q23:日志采集系统用 Kafka 怎么设计?
记忆点:Filebeat → Kafka → Logstash/Flink → ES/HDFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
日志采集架构(ELK + Kafka):
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Server1 │ │ Server2 │ │ Server3 │
│Filebeat │ │Filebeat │ │Filebeat │ ← 日志采集
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
↓ ↓ ↓
┌──────────────────────────────────┐
│ Kafka Cluster │ ← 缓冲层
│ Topic: app-logs (30 partitions) │
└───────────┬──────────┬───────────┘
↓ ↓
┌──────────┐ ┌──────────┐
│ Logstash │ │ Flink │ ← 消费处理
│ (清洗) │ │ (实时分析)│
└────┬─────┘ └────┬─────┘
↓ ↓
┌──────────┐ ┌──────────┐
│ ES/Kibana│ │ HDFS │ ← 存储
│ (检索) │ │ (归档) │
└──────────┘ └──────────┘
关键配置:
retention.ms = 72h (日志保留3天)
cleanup.policy = delete (过期自动删除)
compression.type = lz4 (压缩节省带宽)
partitions = 服务器数 × 2
Q24:消息队列的消息轨迹追踪怎么做?
记忆点:每条消息带 traceId,全链路记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
消息轨迹追踪:
Producer Broker Consumer
│ 生成 traceId │ │
│ 记录: 发送时间+Topic │ │
│ ───── 发送 ─────→ │ 记录: 存储时间+Partition │
│ │ +offset │
│ │ ─── 投递 ──→ │
│ │ 记录: 消费时间│
│ │ +处理结果 │
查询某消息的全链路:
traceId=abc123
→ 生产端: 2026-02-27 10:00:00.123, Topic=orders
→ Broker: 2026-02-27 10:00:00.125, P2, offset=12345
→ 消费端: 2026-02-27 10:00:00.200, 处理成功
RocketMQ 原生支持消息轨迹(msgTraceEnable=true)
Kafka 需要自行实现(Header 中注入 traceId)
Q25:如何做 MQ 的灰度发布和消息迁移?
记忆点:双写 + 双读 + 渐进切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MQ 迁移步骤(如 RabbitMQ → Kafka):
Phase 1: 双写
Producer → RabbitMQ(主)
Producer → Kafka (备)
Consumer 只读 RabbitMQ
Phase 2: 双读验证
Consumer 同时读两个 MQ
对比消息一致性(日志/监控)
Phase 3: 切换
Consumer 切为只读 Kafka
Producer 切为只写 Kafka
RabbitMQ 保留一段时间用于回滚
Phase 4: 下线
确认无问题 → 下线 RabbitMQ
面试口诀速记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MQ 三板斧:解耦、异步、削峰
Kafka 大数据流,Rabbit 企业级,Rocket 做事务
Kafka 快在:顺序写 + 页缓存 + 零拷贝 + 批量 + 分区
ISR 同步副本集,acks=all 最安全
Consumer Group 组内分区独占,组间广播
不丢消息三环:生产确认 + 持久化 + 手动ACK
不重复靠幂等:唯一ID + 去重表 / 唯一约束
顺序靠分区:同 key 同 partition + 单线程消费
积压先扩容,再查原因,后做优化
延迟消息:Rabbit用TTL+DLX,Rocket用等级,通用用Redis ZSet
事务消息:Rocket半消息+回查,通用用本地消息表
这篇文章覆盖了消息队列中间件的核心面试考点。建议在本地搭一个 Kafka 单机版,亲手跑一遍 Producer/Consumer/Consumer Group,比看十遍文章都管用。