消息中间件面试题库(第N轮)
生成时间:2026-05-21 23:12
【面试题】1. Kafka 的 ISR 机制是什么?为什么需要 ISR?ISR 滑动会带来什么问题?
【标准答案】
ISR(In-Sync Replicas)是 Kafka 分区中与 Leader 保持同步的副本集合。
核心机制:
- Leader 维护 ISR 列表,只有 ISR 中的副本才有资格被选为新 Leader
- 副本通过拉取 Leader 的 LE(Log End Offset)来判断同步状态
- 如果 replica.lag.time.max.ms 时间内未追上,会被踢出 ISR
- 追上后会重新加入 ISR
为什么需要 ISR:
- 平衡可用性和持久性。acks=all 时,只需 ISR 副本确认即可,不必等所有副本
- 避免"慢副本"拖垮写入性能
- 故障转移时保证数据不丢失(只从 ISR 选 Leader)
ISR 滑动的问题:
- 抖动问题:网络抖动导致副本频繁进出 ISR,触发频繁重选举
- 数据丢失风险:ISR 缩小到 1,Leader 挂了可能丢数据
- 吞吐下降:ISR 变大,acks=all 的写入延迟增加
生产建议:
- 合理设置 replica.lag.time.max.ms(默认 30s)
- 监控 ISR 收缩告警
- min.insync.replicas ≥ 2,防止 ISR=1 时的数据丢失风险
【面试题】2. RabbitMQ 如何实现延迟消息?对比两种方案的优缺点
【标准答案】
两种主流方案:
方案一:TTL + 死信队列(DLX)
实现原理:
生产者 → TTL队列(设置过期时间)→ 死信交换机 → 死信队列 → 消费者优点:
- 原生支持,无需插件
- 可以为每条消息设置不同的 TTL
缺点:
- 队列 FIFO 特性导致延迟不准:消息按顺序过期,前面的消息阻塞后面的
- 需要为每个延迟时间创建一个队列(延迟时间多时队列爆炸)
- 内存占用大,消息堆积在 TTL 队列
方案二:rabbitmq_delayed_message_exchange 插件
实现原理:
- 安装插件后,创建 x-delayed-message 类型的 Exchange
- 消息带上 x-delay 头,Exchange 延迟投递到目标队列
优点:
- 延迟精确,消息独立计时
- 不需要大量队列,一个 Exchange 搞定多种延迟时间
缺点:
- 需要安装插件,有版本兼容性问题
- 消息存在 Mnesia 表,大量延迟消息有性能瓶颈(官方建议不超过 10 万条)
- 重启后延迟消息可能丢失(取决于持久化配置)
选型建议:
- 延迟时间固定、消息量小 → TTL + DLX
- 延迟时间多变、消息量中等 → 插件方案
- 大规模延迟消息场景 → 考虑 RocketMQ(18 级延迟等级)或时间轮调度
【面试题】3. RocketMQ 为什么不用 ZooKeeper 做注册中心?NameServer 有什么优势?
【标准答案】
RocketMQ 早期版本用 ZooKeeper,后来改用自研 NameServer,原因:
1. 架构简化
- NameServer 是无状态的,节点间无通信,部署运维极简
- ZooKeeper 需要维护 ZAB 协议集群,运维复杂度高
2. 性能更好
- NameServer 只维护 Topic 路由信息,轻量级
- ZooKeeper 的 ZAB 协议有一半以上节点写入才能提交,写性能受限
- RocketMQ 的 Topic 数量可能上万,ZooKeeper 扛不住这个规模的路由心跳
3. AP 优先的设计哲学
- 消息系统的核心是消息存储,不是元数据强一致
- NameServer 允许短暂的路由信息不一致,客户端会重试其他 Broker
- ZooKeeper 是 CP 系统,网络分区时会拒绝写入,影响消息系统可用性
4. 故障恢复更快
- NameServer 无状态,挂了重启即可,数据从 Broker 恢复
- ZooKeeper 挂一半以上节点,集群不可用,恢复时间长
NameServer 的工作机制:
- Broker 每 30s 向所有 NameServer 发心跳,注册 Topic 路由信息
- NameServer 每 10s 扫描失效 Broker,120s 无心跳则剔除
- Producer/Consumer 从 NameServer 获取 Topic 路由,缓存本地并定时更新
总结:NameServer 是"轻量级、AP 架构、运维友好"的取舍,符合消息中间件的场景特点。
【面试题】4. Kafka 消费者组的重平衡(Rebalance)过程是怎样的?如何避免"消费停顿"问题?
【标准答案】
重平衡触发条件:
- 消费者加入或离开消费组
- 消费者心跳超时(session.timeout.ms)
- 消费者处理时间过长(max.poll.interval.ms)
- Topic 分区数变化
重平衡流程(协同模式,新协议):
- 发现阶段:消费者发现需要重平衡(收到回调或心跳失败),发送 JoinGroup 请求
- 选举协调者:第一个加入的消费者成为 Leader
- 分配方案制定:Leader 根据分区分配策略计算分配方案
- 下发方案:Leader 把分配方案发给协调者,协调者下发给所有消费者
- 执行分配:消费者收到分配方案,开始消费
"消费停顿"问题:
- 重平衡期间,整个消费组停止消费,造成消费堆积
- 频繁重平衡 → 消费吞吐量抖动、延迟飙升
如何避免:
1. 合理设置心跳参数
session.timeout.ms=10000 # 心跳超时,建议 10s
heartbeat.interval.ms=3000 # 心跳间隔,建议 3s
max.poll.interval.ms=300000 # 两次 poll 最大间隔,根据业务处理时长设置2. 使用静态成员(Static Membership)
group.instance.id=consumer-1 # 固定消费者 ID- 消费者短暂离开不会触发重平衡,回来后继续消费原分区
3. 减少消费组成员变化
- 避免消费者频繁上下线(如 K8s 滚动更新)
- 合理设置 max.poll.records,避免一次拉取太多导致处理超时
4. 使用 Incremental Cooperative 协议(Kafka 2.4+)
- 不再是"stop-the-world",而是增量重分配
- 只重新分配受影响的分区,其他分区继续消费
总结:心跳超时、处理超时、消费者抖动是重平衡三大元凶,参数调优 + 静态成员 + 增量协议是三把钥匙。
【面试题】5. 如何保证消息的顺序消费?分别在 Kafka 和 RocketMQ 中如何实现?
【标准答案】
为什么顺序消费难:
- 分区/队列并行消费,不同分区的消息无顺序保证
- 同一分区内,消费者多线程并发处理,顺序被打乱
- 消息重试时,可能插队到队列尾部
Kafka 的顺序消费方案:
1. 分区内有序
- Kafka 只保证分区内消息有序(生产者写入顺序)
- 一个分区只能被消费组内的一个消费者消费
2. 实现全局有序
- Topic 只创建 1 个分区(牺牲并发,不推荐)
- 更好的方案:业务维度分区,同一业务 ID 的消息发到同一分区
3. 生产端指定分区键
ProducerRecord<String, String> record =
new ProducerRecord<>("topic", "orderId-123", message);
// orderId-123 的消息总是路由到同一分区4. 消费端单线程处理
- 一个分区对应一个消费者线程
- 如果要多线程,需要自己在应用层做内存队列 + 单线程消费
RocketMQ 的顺序消费方案:
1. 全局顺序消息
- Topic 创建时设置队列为 1
- 性能差,一般不用
2. 分区顺序消息
Message msg = new Message("Topic", "Tag", "orderId-123", message.getBytes());
SendResult result = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long orderId = (Long) arg;
int index = (int) (orderId % mqs.size());
return mqs.get(index);
}
}, orderId);- MessageQueueSelector 根据 orderId 选择固定队列
- 同一 orderId 的消息发到同一队列
3. 消费端顺序监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 单线程顺序消费
return ConsumeOrderlyStatus.SUCCESS;
}
});关键区别:
- Kafka:分区维度有序,消费端自己控制线程
- RocketMQ:支持 MessageListenerOrderly,框架层保证队列内有序
生产建议:
- 业务维度的顺序(如订单 ID),用分区键/队列选择器
- 顺序消费会牺牲并发度,评估是否真的需要全局有序
- 消费失败时,不要立即重试,可能破坏顺序,建议发到重试队列
【面试题】6. 消息积压了怎么处理?线上应急的完整流程是什么?
【标准答案】
1. 发现积压
监控告警:
- 消费滞后量(Consumer Lag)= Log End Offset - Consumer Offset
- Kafka:
kafka-consumer-groups --describe - RocketMQ: 控制台查看消费滞后
- 设置告警阈值(如积压 > 10 万条)
2. 快速止血(5 分钟内)
方案 A:扩容消费者
- 增加消费者实例数量
- Kafka: 确保分区数 ≥ 消费者数,否则扩容无效
- RocketMQ: 增加队列数,再增加消费者
方案 B:临时降级
- 关闭非核心业务的消息消费,优先保障核心业务
- 比如营销消息暂停消费,只消费订单消息
方案 C:跳过积压消息(紧急情况)
- 重置消费位点,跳过积压消息
- Kafka:
--reset-offsets --to-latest - RocketMQ: 控制台重置位点
- 跳过的消息需要后续补偿(如数据库查漏补)
3. 消化积压(短期 1-2 小时)
扩容策略:
- 消费者数量 = 积压量 / 目标处理时间 / 单消费者 QPS
- 例:积压 100 万条,目标 1 小时消化,单消费者 100 QPS → 需要 3 个消费者
临时队列方案:
- 积压 Topic 的消息转发到临时 Topic
- 临时 Topic 分区数是原来的 5-10 倍
- 扩容消费者消费临时 Topic
- Kafka: 使用 MirrorMaker 或自己写程序转发
- RocketMQ: 写个临时消费者程序
4. 根因分析(事后复盘)
常见原因:
- 消费逻辑有慢操作(如数据库慢查询、外部 API 超时)
- 消费者实例挂了或频繁 Full GC
- 下游服务不可用,消息处理失败重试
- 消息量突增(如活动、定时任务触发)
5. 长期预防
- 设置消息过期时间,自动丢弃超时消息
- 监控消费耗时,慢消息告警
- 限流:生产端限流,防止消息量过大
- 死信队列:多次重试失败的消息进 DLQ,人工处理
总结口诀: 发现积压 → 快速扩容 → 临时队列 → 降级跳过 → 根因复盘 → 监控预防
【面试题】7. Kafka 的零拷贝原理是什么?为什么比普通 IO 快?
【标准答案】
传统 IO 的数据拷贝流程(读取磁盘 + 发送网络):
磁盘 → 内核缓冲区 → 用户缓冲区 → 内核 Socket 缓冲区 → 网卡
① ② ③ ④ ⑤- 4 次数据拷贝:磁盘到内核、内核到用户、用户到内核、内核到网卡
- 4 次上下文切换:用户态 ↔ 内核态切换 4 次
- 用户空间的数据是"多余的",实际不需要
零拷贝(sendfile 系统调用):
磁盘 → 内核 PageCache → 网卡
① ② ③- 2 次数据拷贝:DMA 直接从 PageCache 到网卡
- 2 次上下文切换:用户态不参与,减少 CPU 开销
- 数据不经过用户空间,直接从内核缓冲区到网卡
Kafka 的零拷贝应用:
FileChannel.transferTo(position, count, socketChannel);- 调用 Linux 的 sendfile 系统调用
- Broker 直接把日志文件从 PageCache 发送到网卡
- 不需要把消息反序列化到 JVM 堆内存
为什么 Kafka 能用零拷贝:
- 消息是追加写入,顺序读,PageCache 命中率高
- 消息格式固定,不需要在应用层修改
- Producer 和 Consumer 直接读写日志文件
性能对比:
- 传统 IO:4 次拷贝 + 4 次切换,CPU 参与多次
- 零拷贝:2 次拷贝 + 2 次切换,CPU 只负责协调
总结: 零拷贝的核心是"数据不经过用户态",利用操作系统的 sendfile 和 DMA,减少 CPU 和内存拷贝开销。Kafka 的高吞吐,零拷贝贡献了 30% 以上的性能提升。
【面试题】8. RabbitMQ、Kafka、RocketMQ 三者怎么选型?各自的适用场景是什么?
【标准答案】
1. RabbitMQ
特点:
- Erlang 开发,AMQP 协议实现
- 功能丰富:多种 Exchange、路由键、死信队列、延迟队列、优先级队列
- 消息可靠性高,支持事务和确认机制
- 吞吐量中等(单机万级 QPS),延迟低(毫秒级)
适用场景:
- 业务系统内部通信,需要复杂路由
- 订单、支付等可靠性要求高的场景
- 企业级应用,团队熟悉 AMQP 协议
- 消息量不大(单机万级),延迟敏感
不适用:
- 大数据日志收集(吞吐不够)
- 高并发削峰(单机瓶颈)
2. Kafka
特点:
- Scala 开发,分布式架构
- 吞吐量极高(单机百万级 QPS),适合大数据场景
- 消息顺序写入磁盘,零拷贝 + PageCache
- 消息持久化,支持消息回溯
- 功能相对简单,路由能力弱
适用场景:
- 日志收集、大数据管道
- 实时数据流处理(Kafka Streams)
- 高吞吐削峰场景
- 需要消息回溯(如流处理任务重跑)
不适用:
- 需要复杂路由、延迟队列(需要二次开发)
- 消息可靠性要求极高、不允许丢失(需要仔细配置)
3. RocketMQ
特点:
- Java 开发,阿里出品
- 吞吐量高(单机十万级 QPS),可靠性高
- 功能丰富:事务消息、延迟消息、顺序消息、消息过滤
- 国内社区活跃,文档完善
适用场景:
- 电商、金融等可靠性要求高的场景
- 需要事务消息(分布式事务)
- 需要延迟消息(如订单超时取消)
- 国内团队,中文文档友好
不适用:
- 国际化项目(国外社区小)
- 需要极致吞吐(不如 Kafka)
选型决策表:
| 维度 | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|
| 吞吐量 | 万级 | 百万级 | 十万级 |
| 延迟 | 毫秒级 | 毫秒级 | 毫秒级 |
| 可靠性 | 高 | 中(需配置) | 高 |
| 功能丰富度 | 高 | 中 | 高 |
| 延迟队列 | 支持(插件) | 不支持 | 原生支持 |
| 事务消息 | 支持 | 支持(事务型生产者) | 原生支持 |
| 学习曲线 | 低 | 中 | 中 |
| 社区生态 | 成熟 | 极其成熟 | 国内成熟 |
总结口诀: 小量复杂用 Rabbit,海量日志用 Kafka,电商金融用 RocketMQ。
【面试题】9. RocketMQ 的半消息机制是如何实现分布式事务的?详细说明流程
【标准答案】
RocketMQ 事务消息的核心思想: "先发半消息,再执行本地事务,根据事务结果决定提交或回滚消息"
半消息(Half Message):
- 暂时不能被消费的消息
- 存储在 Broker,但对消费者不可见
- 状态:Prepare(半消息)、Commit(可消费)、Rollback(删除)
完整流程:
1. 生产者发送半消息 → Broker
2. Broker 返回半消息发送成功
3. 生产者执行本地事务
4. 生产者根据本地事务结果,发送提交/回滚请求给 Broker
- 本地事务成功 → Commit,消息对消费者可见
- 本地事务失败 → Rollback,消息被删除
5. 如果生产者失联(第 4 步没发生),Broker 回查生产者
6. 生产者返回事务状态,Broker 执行对应操作关键机制:事务回查
为什么需要回查?
- 生产者发送半消息后,可能宕机、网络断开
- Broker 收不到 Commit/Rollback,半消息处于"悬而未决"状态
回查流程:
- Broker 定时扫描半消息(默认 60s)
- 发起回查请求给生产者
- 生产者查询本地事务状态(查数据库)
- 返回 Commit/Rollback/Unknown
- 如果返回 Unknown,Broker 继续回查(最多回查次数可配置)
代码示例:
// 1. 创建事务监听器
TransactionListener transactionListener = new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务
try {
doLocalTransaction();
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 回查本地事务状态(查数据库)
boolean success = checkTransactionStatus(msg.getKeys());
return success ? LocalTransactionState.COMMIT_MESSAGE :
LocalTransactionState.ROLLBACK_MESSAGE;
}
};
// 2. 发送事务消息
TransactionMQProducer producer = new TransactionMQProducer("TransactionProducerGroup");
producer.setTransactionListener(transactionListener);
producer.start();
Message msg = new Message("Topic", "Tag", "orderId-123", message.getBytes());
TransactionSendResult result = producer.sendMessageInTransaction(msg, null);与本地消息表的对比:
- 本地消息表:消息存储在业务数据库,需要定时任务扫描
- 事务消息:消息存储在 Broker,Broker 主动回查,解耦更彻底
注意事项:
- 本地事务要幂等,回查可能执行多次
- 回查逻辑要轻量,避免阻塞 Broker
- 半消息有超时时间,超时自动回滚
【面试题】10. 如何设计一个百万级 QPS 的消息系统架构?
【标准答案】
从四个维度设计:生产端、Broker 集群、消费端、可靠性保障
一、生产端设计
1. 批量发送 + 压缩
- Kafka: linger.ms + batch.size,批量攒消息
- 压缩算法:LZ4 / Snappy(压缩比 50%,CPU 开销小)
- 批量发送可提升 5-10 倍吞吐
2. 异步发送
- 消息先写入本地缓冲区,异步线程批量发送
- 使用异步回调处理发送结果
- 避免同步发送阻塞业务线程
3. 分区策略
- 生产端根据业务 ID(如用户 ID)哈希选分区
- 避免单个分区热点
- 分区键设计要均匀(用户 ID 比订单 ID 更均匀)
二、Broker 集群设计
1. 集群规模
- 单机 Kafka 百万 QPS 需要高性能硬件(SSD + 万兆网卡)
- 一般 10-20 台 Broker,单机 10 万 QPS,集群 100-200 万 QPS
- 分区数:集群总 QPS / 单分区 QPS(单分区一般 1-2 万 QPS)
2. 分区规划
- Topic 分区数 = 消费者并发度需求
- 例:100 个分区,单分区 1 万 QPS,总吞吐 100 万
- 分区数不宜过多:会增加 Broker 内存开销和选举时间
3. 副本策略
- 副本数:生产环境建议 3 副本
- min.insync.replicas = 2,确保至少 2 个副本写入成功
- 跨机架/机房部署副本,防机架故障
4. 硬件配置
- 磁盘:NVMe SSD,顺序写性能关键
- 内存:32GB+,PageCache 缓存热点数据
- 网络:万兆网卡,带宽是瓶颈
- CPU:多核,压缩和网络开销
三、消费端设计
1. 消费者并发度
- 消费者数 ≤ 分区数(Kafka),消费者数 ≤ 队列数(RocketMQ)
- 根据消息处理耗时调整:消费者数 = 目标 QPS / 单消费者处理能力
2. 消费者线程模型
- Kafka Consumer 单线程拉取,多线程处理(线程池)
- 注意:手动提交 Offset,避免处理未完成就提交
3. 多级消费架构
Broker → 消费者服务(只负责拉消息)→ 本地消息队列(如 Disruptor)→ 处理线程池- 消费者服务与 Broker 解耦,可以水平扩展
- 本地队列缓冲,削峰填谷
4. 消费幂等
- 消费者必须实现幂等逻辑
- 方案:唯一 ID + 数据库去重表 / Redis SetNX
四、可靠性保障
1. 生产端确认
- Kafka: acks=all,确保所有 ISR 副本写入成功
- RocketMQ: 同步发送,等待 Broker 返回
2. 消费端手动提交
- 业务处理成功后再提交 Offset
- 避免消息处理失败但 Offset 已提交导致丢消息
3. 死信队列
- 多次重试失败的消息进入 DLQ
- 人工处理或自动告警
4. 监控告警
- 消费滞后量(Lag)监控
- Broker CPU/内存/磁盘/网络监控
- 生产者发送失败率监控
五、架构示意
[业务系统] → [生产者集群]
↓ 批量 + 异步 + 压缩
[Broker 集群 10-20 台]
[Topic: 100 分区, 3 副本]
↓
[消费者集群] → [本地队列 Disruptor] → [处理线程池] → [业务逻辑]关键参数配置:
# Kafka Broker
num.network.threads=8
num.io.threads=8
socket.send.buffer.bytes=1024000
socket.receive.buffer.bytes=1024000
# Kafka Producer
linger.ms=10
batch.size=65536
compression.type=lz4
acks=all
# Kafka Consumer
fetch.min.bytes=102400
fetch.max.wait.ms=500
max.poll.records=1000总结: 百万级 QPS = 批量发送 + 异步发送 + 分区并行 + 消费者集群 + SSD + 万兆网卡。核心是"并行度"和"批量"。
本套面试题覆盖:ISR 机制、延迟消息、NameServer 架构、重平衡、顺序消费、消息积压、零拷贝、选型对比、事务消息、高并发架构设计。