Skip to content

消息中间件面试题库(第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 分区数变化

重平衡流程(协同模式,新协议):

  1. 发现阶段:消费者发现需要重平衡(收到回调或心跳失败),发送 JoinGroup 请求
  2. 选举协调者:第一个加入的消费者成为 Leader
  3. 分配方案制定:Leader 根据分区分配策略计算分配方案
  4. 下发方案:Leader 把分配方案发给协调者,协调者下发给所有消费者
  5. 执行分配:消费者收到分配方案,开始消费

"消费停顿"问题:

  • 重平衡期间,整个消费组停止消费,造成消费堆积
  • 频繁重平衡 → 消费吞吐量抖动、延迟飙升

如何避免:

1. 合理设置心跳参数

properties
session.timeout.ms=10000  # 心跳超时,建议 10s
heartbeat.interval.ms=3000  # 心跳间隔,建议 3s
max.poll.interval.ms=300000  # 两次 poll 最大间隔,根据业务处理时长设置

2. 使用静态成员(Static Membership)

properties
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. 生产端指定分区键

java
ProducerRecord<String, String> record = 
    new ProducerRecord<>("topic", "orderId-123", message);
// orderId-123 的消息总是路由到同一分区

4. 消费端单线程处理

  • 一个分区对应一个消费者线程
  • 如果要多线程,需要自己在应用层做内存队列 + 单线程消费

RocketMQ 的顺序消费方案:

1. 全局顺序消息

  • Topic 创建时设置队列为 1
  • 性能差,一般不用

2. 分区顺序消息

java
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. 消费端顺序监听器

java
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 的零拷贝应用:

java
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)

选型决策表:

维度RabbitMQKafkaRocketMQ
吞吐量万级百万级十万级
延迟毫秒级毫秒级毫秒级
可靠性高中(需配置)高
功能丰富度高中高
延迟队列支持(插件)不支持原生支持
事务消息支持支持(事务型生产者)原生支持
学习曲线低中中
社区生态成熟极其成熟国内成熟

总结口诀: 小量复杂用 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 继续回查(最多回查次数可配置)

代码示例:

java
// 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] → [处理线程池] → [业务逻辑]

关键参数配置:

properties
# 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 架构、重平衡、顺序消费、消息积压、零拷贝、选型对比、事务消息、高并发架构设计。