RabbitMQ 面试重点难点
一、基础概念
1.1 核心模型
| 组件 |
说明 |
| Producer |
生产者,发送消息 |
| Consumer |
消费者,接收消息 |
| Queue |
队列,存储消息 |
| Exchange |
交换机,路由消息 |
| Binding |
绑定,Exchange 与 Queue 的关联规则 |
| Virtual Host |
虚拟主机,权限隔离单元 |
1.2 Exchange 类型
| 类型 |
路由规则 |
常见场景 |
| Direct |
Routing Key 精确匹配 |
单播、指定路由 |
| Topic |
Routing Key 通配符匹配(# 匹配0+词, * 匹配1词) |
多播、按模式分发 |
| Fanout |
忽略 Routing Key,广播到所有绑定队列 |
广播、全局通知 |
| Headers |
根据消息头属性匹配(x-match: all/any) |
复杂路由条件 |
二、消息可靠性(高频重点)
2.1 生产者可靠性
1) 事务机制(Tx)
channel.txSelect();
channel.basicPublish(...);
channel.txCommit(); // 或 txRollback()
- 缺点:同步阻塞,吞吐量极低(约下降 250 倍)
- 面试点:知道有事务但实际不用,改用 Confirm
2) 发送方确认(Publisher Confirm)
- 普通 Confirm:每发一条 waitForConfirms()
- 批量 Confirm:每 N 条 waitForConfirmsOrDie()
- 异步 Confirm:添加 ConfirmListener,回调处理 ack/nack(性能最优,推荐)
channel.confirmSelect();
// 异步监听
channel.addConfirmListener((deliveryTag, multiple) -> {
// ack 回调
}, (deliveryTag, multiple) -> {
// nack 回调
});
2.2 消息可靠存储
2.3 消费者可靠性
ACK 机制
| 类型 |
说明 |
是否会丢弃 |
| 自动 ACK |
消费者收到即确认 |
可能丢消息(consumer 宕机) |
| 手动 ACK |
处理完业务才确认 |
不丢,但需处理重复 |
| 否定 ACK (nack) |
拒绝消息 |
依赖 requeue 参数 |
// 手动确认
channel.basicConsume(queue, false, consumer); // autoAck = false
// 成功处理
channel.basicAck(envelope.getDeliveryTag(), false);
// 处理失败,重新入队
channel.basicNack(envelope.getDeliveryTag(), false, true);
// 处理失败,丢弃
channel.basicNack(envelope.getDeliveryTag(), false, false);
预取值(QoS / Prefetch Count)
channel.basicQos(1); // 每次只发1条,处理完再发下一条
2.4 幂等性问题(必问)
- RabbitMQ 的 at-least-once 语义 → 消费者需自行保证幂等
- 方案:消息 ID 去重表、业务主键判重、Redis Set 去重
三、高级特性与难点
3.1 死信队列(DLQ)
死信来源:
- 消息被拒绝并
requeue=false
- 消息 TTL 过期
- 队列达到最大长度
配置方式:
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange");
args.put("x-dead-letter-routing-key", "dlx.rk");
channel.queueDeclare("queue", true, false, false, args);
面试点:死信队列的应用 → 延迟队列、异常消息处理
3.2 延迟队列
- 实现方式 :死信队列 + 消息 TTL(
x-message-ttl)
- 注意:队列级别的 TTL 过期行为更精确(消息级别的 TTL 可能在队列头部过期才检查)
- 应用:订单超时取消、定时任务
3.3 优先级队列
args.put("x-max-priority", 10); // 0-255
channel.queueDeclare("queue", true, false, false, args);
- 注意:只有堆积的消息才生效,消费者空闲时优先级无效
3.4 惰性队列(Lazy Queue)
- 从 RabbitMQ 3.6 引入,消息直接写入磁盘,内存占用小
- 适用:海量消息堆积场景
- 缺点:吞吐量低于正常队列
3.5 仲裁队列(Quorum Queue,3.8+)
- 基于 Raft 协议,数据在集群节点间复制
- 优点:高可用、数据安全
- 与镜像队列的区别:Quorum Queue 强一致性,镜像队列最终一致性
四、集群与高可用(高频)
4.1 集群模式
| 模式 |
存储方式 |
可用性 |
| 普通集群 |
元数据在所有节点,数据在单一节点 |
非 HA,节点挂则队列不可用 |
| 镜像队列 |
主从模式,队列内容在所有镜像节点 |
HA,master 挂则 slave 提升 |
| 仲裁队列 |
Raft 共识,多数派写入 |
HA,强一致性 |
4.2 镜像队列配置
rabbitmqctl set_policy ha-all ".*" '{"ha-mode":"all","ha-sync-mode":"automatic"}'
4.3 脑裂问题
- RabbitMQ 的镜像队列可能出现脑裂(网络分区)
- 处理策略 :
pause-minority:少数派节点暂停(推荐)
ignore:忽略分区(默认,继续运行但数据不一致)
autoheal:重启获胜分区节点
4.4 节点类型
| 类型 |
说明 |
| RAM Node |
元数据在内存,性能高,重启元数据丢失 |
| Disk Node |
元数据在磁盘,集群至少 1 个 Disk 节点 |
五、性能与优化
5.1 性能瓶颈
- 磁盘 IO:持久化消息的写入瓶颈
- 内存:消息堆积导致内存暴涨
- 网络:集群间复制延迟
5.2 优化策略
| 措施 |
说明 |
| Prefetch 控制 |
basicQos(预取值) 防止消息堆积 |
| 批量确认 |
批量 Confirm 替代逐条确认 |
| 惰性队列 |
消息直接写磁盘,减少内存压力 |
| 消息压缩 |
gzip 压缩大消息体 |
| 分片 Queue |
多 Queue 分散负载 |
六、与其他 MQ 对比(高频)
| 特性 |
RabbitMQ |
Kafka |
RocketMQ |
| 协议 |
AMQP |
自定义(TCP) |
自定义 |
| 吞吐量 |
万级 |
百万级 |
十万级 |
| 延迟 |
微秒级 |
毫秒级 |
毫秒级 |
| 消息顺序 |
单队列有序 |
Partition 内有序 |
Queue 内有序 |
| 消息可靠性 |
高 |
高 |
高 |
| 路由能力 |
极强(多种 Exchange) |
弱(Topic + Partition) |
强(Tag + 过滤) |
| 适用场景 |
企业级、业务解耦、灵活路由 |
大数据、日志、流处理 |
业务消息、金融级 |
七、高频面试题
7.1 消息丢失场景与解决方案
| 场景 |
原因 |
解决 |
| 生产者丢失 |
网络问题、Exchange 不存在 |
Confirm 机制 + Return Listener |
| Broker 丢失 |
消息未持久化、宕机 |
三要素持久化 + 镜像/仲裁队列 |
| 消费者丢失 |
自动 ACK 后宕机 |
手动 ACK + basicQos |
7.2 消息重复消费
- 原因:生产者重试、网络重传、消费者 ACK 超时重发
- 解决:幂等性(唯一 ID + 去重表)
7.3 消息堆积处理
- 临时方案:新建 Queue + 临时消费者批量消费 + 持久化到 DB
- 长期方案 :增加消费者(
basicQos + 水平扩容)、改用惰性队列
7.4 顺序消息
- 单队列单消费者:天然有序(但吞吐低)
- 多队列按业务 Key Hash:同一业务 Key 路由到同一队列
- 注意:RabbitMQ 不保证顺序,需应用层自行设计
7.5 消息回溯
- RabbitMQ 不支持 Kafka 的 Offset 重置
- 方案:消息持久化到 DB / Elasticsearch,按需重新投递
八、Spring Boot 集成
8.1 核心配置
spring:
rabbitmq:
host: localhost
port: 5672
publisher-confirm-type: correlated # Confirm 异步回调
publisher-returns: true # Return 回调(路由不可达)
listener:
simple:
acknowledge-mode: manual # 手动 ACK
prefetch: 1 # 预取值
8.2 关键注解
| 注解 |
用途 |
@EnableRabbit |
启用 RabbitMQ 注解模式 |
@RabbitListener |
声明监听队列 |
@RabbitHandler |
方法级别分发 |
@Exchange / @Queue / @QueueBinding |
声明绑定关系 |
九、整体执行流程(一条消息的完整生命周期)
9.1 生产者发送阶段
Producer → Channel → Exchange → Binding匹配 → Queue(s)
- 建立连接 :Producer 通过 TCP 连接到 RabbitMQ Node(默认 5672 端口),在 TCP 连接上创建 Channel(轻量级多路复用,一个 Connection 可开多个 Channel)
- 声明 Exchange/Queue :
channel.exchangeDeclare() / channel.queueDeclare() --- 元数据写入 Mnesia(RabbitMQ 内嵌数据库)
- 发送消息 :
channel.basicPublish(exchange, routingKey, props, body)
- Exchange 路由:根据 Exchange 类型 + Binding 规则,将消息路由到 0/N 个队列
- Mandatory 回退 :若路由不到任何队列 +
mandatory=true → 触发 basic.return;否则消息静默丢弃
- 持久化:若消息标记为持久化 → 写入磁盘(Queue Index + Message Store)
- Confirm 回调 :若开启 Confirm 模式 → 消息写入磁盘/路由完成后 → Broker 回送
basic.ack
9.2 Broker 内部流转
Exchange → Binding Table 匹配 → Queue(内存/磁盘)→ 等待消费
- Binding Table:Exchange 内部维护的哈希表,key = routingKey pattern,value = 绑定队列列表
- Topic Exchange 匹配 :将 routingKey 按
. 分词,与 Binding 的 pattern 做通配符匹配(DFA/树形优化)
- Queue 内部 :维护一个 FIFO 的
ready 消息列表(消费者可见),以及 unacked 列表(已投递未确认)
9.3 消费者消费阶段
Broker → Basic.Deliver → Consumer → 业务处理 → Basic.Ack
- 推模式(Push) :
basic.consume 注册 Consumer,Broker 主动推送消息(最常用)
- 拉模式(Pull) :
basic.get 消费者主动拉取(轮询,效率低,不推荐)
- QoS 控制:根据 Prefetch Count 决定一次推送多少条未确认消息
- 消息投递 :Broker 从 Queue 的 ready 队列头部取出消息,封装为
basic.deliver 帧发送给 Consumer
- ACK 处理 :
- 收到
basic.ack → 从 unacked 列表移除消息
- 收到
basic.nack + requeue=true → 重新入队 ready 列表头部或尾部
- Channel 关闭 / 连接断开 → 所有 unacked 消息自动重新入队(at-least-once 语义的来源)
9.4 完整时序图
Producer Exchange Queue Consumer
| | | |
|-- basic.publish -------->| | |
| |-- routing ----------->| |
| | |-- basic.deliver ----->|
| | | |-- 业务处理
| | |<-- basic.ack ---------|
|-- basic.ack (Confirm) -->| | |
十、底层实现原理
10.1 AMQP 协议帧结构(Wire Level)
RabbitMQ 基于 AMQP 0-9-1 协议,所有通信均为二进制帧:
Frame Header (7 bytes) Payload Frame End
+--------+---------+------+------------+---------------+------+
| type | channel | size | body | (属性+体) | 0xCE |
| (1 B) | (2 B) |(4 B) | (可变长) | | (1 B)|
+--------+---------+------+------------+---------------+------+
- type:1=METHOD, 2=HEADER, 3=BODY, 4=HEARTBEAT
- channel:多路复用标识,同一个 Connection 上区分不同 Channel
- 总开销:每个帧约 8 字节头部 + 1 字节尾部
关键方法帧示例:
basic.publish = Class(60) + Method(40)
basic.deliver = Class(60) + Method(60)
basic.ack = Class(60) + Method(80)
10.2 Erlang/OTP 架构
RabbitMQ 使用 Erlang/OTP 编写,基于 Actor 模型:
| 组件 |
Erlang 进程 |
职责 |
| Connection |
rabbit_reader |
解析 AMQP 帧、协议解析 |
| Channel |
rabbit_channel |
命令处理、Exchange 路由、Queue 交互 |
| Queue |
rabbit_amqqueue_process |
消息入队/出队、ACK 管理、状态维护 |
| Exchange |
rabbit_exchange_type_* |
路由匹配(gen_server 实现) |
| Message Store |
rabbit_msg_store |
消息体持久化(文件系统) |
| Queue Index |
多种实现(rabbit_queue_index) |
消息序号、位置索引 |
关键调度 :每个 Queue 是一个独立的 Erlang 进程,通过 进程间消息传递(而非共享内存)与 Channel 进程通信 → 天然隔离,单 Queue 故障不影响其他 Queue
10.3 消息存储引擎
分层存储架构
+-------------------+
| Queue Index | → 消息逻辑位置(seq_id → 文件偏移)
+-------------------+
|
+-------------------+
| Message Store | → 消息体二进制数据(文件追加写)
+-------------------+
|
+-------------------+
| 文件系统 (NTFS) |
+-------------------+
消息写磁盘流程(持久化消息)
1. 消息写入 Queue Index(记录 seq_id, 文件位置等元数据)
2. 消息体写入 Message Store(追加写入 .rdq 文件)
3. 文件刷盘(按配置的 sync_interval,默认 200ms 或每 N 条)
4. 返回写入成功 → 触发 Confirm ack
文件管理
- 文件后缀 :
.rdq(消息存储文件),每个文件默认 16MB
- 文件回收:文件内所有消息被确认后 → 删除/垃圾回收
- 写入策略 :
- 小于 64KB 的消息 → 直接写入 Message Store 文件(按消息体大小紧凑排列)
- 大于 64KB 的消息 → 放入单独的大消息文件(避免小消息被大消息阻塞确认回收)
10.4 Queue 内部状态划分(核心难点)
RabbitMQ 将队列消息划分为 4 种状态:
| 状态 |
存储位置 |
说明 |
| Alpha |
内存 |
可直接投递的消息 |
| Beta |
内存 + 磁盘索引 |
在内存中,但消息体在磁盘(仅索引在内存) |
| Gamma |
磁盘索引 |
消息体和索引都在磁盘,但有进程引用 |
| Delta |
磁盘 |
消息体在磁盘,无进程引用(大量堆积时) |
状态转换(面试高频):
消息入队
↓
┌─── Alpha (内存) ───┐
│ ↓ 内存不足 │
│ Beta (索引在内存) │
│ ↓ │
│ Gamma (磁盘索引) │
│ ↓ 堆积严重 │
└── Delta (全磁盘) ────┘
- 投递效率:Alpha > Beta > Gamma > Delta
- 触发条件 :
vm_memory_high_watermark(默认 40%)→ 触发换出(swap out)
- 惰性队列:消息直接进入 Delta,跳过 Alpha/Beta
10.5 网络与 IO 线程模型
┌─────────────────────────────────┐
│ TCP Connection (ranch) │
│ ↓ │
│ rabbit_reader (Erlang进程) │ ← 解析 AMQP 帧
│ ↓ │
│ rabbit_channel (Erlang进程) │ ← 处理语义、路由
│ ↓ │
│ rabbit_amqqueue_process │ ← Queue 维护
│ ↓ │
│ rabbit_msg_store / 文件 IO │ ← 磁盘持久化
└─────────────────────────────────┘
- Ranch:Erlang 的 socket 管理库,接受 TCP 连接
- 每个连接一个 Erlang 进程 :
rabbit_reader 负责帧读取和协议解析
- 每个 Channel 一个 Erlang 进程 :
rabbit_channel 处理语义层
- 异步 IO :Erlang 的文件操作使用异步线程池(
erlang:open_port({spawn, ...}, ...) 或 +A 标志指定线程数)
10.6 流量控制(Flow Control)
基于 Credit 的流控机制
RabbitMQ 内部采用 Credit Flow 机制防止生产者淹没消费者:
Process A (Producer) Process B (Consumer)
| |
|-- Initial Credits: 200 ---->|
| |
|-- Message ------------------>| (消耗1 credit)
|-- Message ------------------>| (消耗1 credit)
| |
|<-- Credit Return ------------| (处理完 N 条后归还 credit)
- Connection → Channel → Queue 整条链路均受 Credit 控制
- 当 Queue 进程堆积(mailbox 过大)→ 停止发放 Credit → 上游 Channel 阻塞 → 最终 Connection 阻塞 → 生产者感受到 TCP backpressure
- 注意:这是 RabbitMQ 内部流控,与 Prefetch(消费者流控)不同
内存水位控制
# 默认配置
vm_memory_high_watermark = 0.4 # 内存使用超 40% → 阻塞生产者
vm_memory_high_watermark_paging_ratio = 0.5 # 达到水位的 50% 时开始换出到磁盘
10.7 Erlang VM(BEAM)相关
- 调度器:BEAM 默认使用 CPU 核心数个调度器,每个调度器拥有独立的运行队列
- 进程调度:抢占式调度,每个 Erlang 进程约 300 字(~2.4KB)开销
- ETS 表:Exchange 路由表、Binding 表存储在 ETS(内存哈希表),读取无锁
- Mnesia:元数据(队列声明、绑定关系、用户权限)存储在 Mnesia(分布式数据库),RAM 节点存内存副本,Disk 节点存磁盘
10.8 镜像队列同步原理(Guaranteed Replication)
Master Node Slave Node 1 Slave Node 2
| | |
|-- publish msg ---------->|-- GM broadcast ------->|
| | |
|<-- pg_rv (ok) -----------|<-- pg_rv (ok) --------|
| | |
|-- ack to producer -------| |
- GM (Guaranteed Multicast):Erlang 的可靠广播库,基于 RPC + 重试
- 写入时序:Master 先持久化 → 广播到 Slave → 等待多数派确认 → 回复 Producer
- 提升时序:Master 宕机 → Slave 中最新的成为新 Master → 重新绑定 Consumer → 重新投递 unacked 消息
- 同步方式 :
automatic:新 Slave 加入时自动同步全部消息
manual:手动触发 rabbitmqctl sync_queue
10.9 仲裁队列(Quorum Queue)底层原理
- Raft 实现 :使用 Erlang 的
ra 库实现 Raft 共识算法
- 写入过程 :
- Leader 接收消息 → 追加本地的 WAL (Write Ahead Log)
- 并行发送
AppendEntries RPC 到 Follower
- 多数派(N/2 + 1)写入成功 → 提交 → 回复 Producer
- 与镜像队列区别 :
- 镜像队列:异步 GM 广播,最终一致性
- 仲裁队列:同步 Raft 写入,强一致性(线性一致性读)
- 仲裁队列:不丢失已提交消息(即使全部节点宕机再恢复)
- 镜像队列:脑裂可能导致数据不一致
十一、总结记忆口诀
一模型两角色,四种交换机 持久化三要素,Confirm 保发送 手动 ACK + QoS,幂等防重复 镜像仲裁抗宕机,死信 TTL 做延迟 惰性队列解堆积,脑裂分区 autoheal
Kafka 面试重点难点
一、核心概念
1.1 基础模型
| 组件 |
说明 |
| Producer |
生产者,发布消息到 Topic |
| Consumer |
消费者,从 Topic 拉取消息 |
| Consumer Group |
消费组,组内消费者分摊 Partition,组间独立消费 |
| Topic |
逻辑主题,消息分类 |
| Partition |
分片,Topic 内部分区,并行最小单位 |
| Segment |
段,Partition 文件在磁盘上的分段 |
| Offset |
偏移量,Partition 内消息的唯一序号 |
| Broker |
Kafka 服务器节点 |
| Controller |
集群控制器,负责 Leader 选举和元数据管理 |
| ZooKeeper / KRaft |
元数据存储和集群协调 |
1.2 Topic 与 Partition
Topic: orders
├── Partition-0 (Leader on Broker1, Follower on Broker2,3)
├── Partition-1 (Leader on Broker2, Follower on Broker1,3)
└── Partition-2 (Leader on Broker3, Follower on Broker1,2)
- 分区内有序:Offset 单调递增,消息顺序保证
- 跨分区无序:不同 Partition 间不保证顺序
- 并行度上限:= Partition 数量(一个 Partition 只能被同一 Group 内一个 Consumer 消费)
二、消息存储与文件结构(高频)
2.1 磁盘文件布局
/tmp/kafka-logs/
└── orders-0/ # Partition 目录: {topic}-{partition}
├── 00000000000000000000.log # 消息数据文件(追加写)
├── 00000000000000000000.index # 偏移量索引(稀疏,每 4KB 记录一条)
├── 00000000000000000000.timeindex # 时间戳索引
├── 00000000000000001000.log
├── 00000000000000001000.index
├── 00000000000000001000.timeindex
└── leader-epoch-checkpoint
2.2 Segment 结构
.log 文件(消息体):
[Message] [Message] [Message] ...
每个 Message = 长度(4B) + CRC(4B) + 魔数(1B) + 属性(1B) +
时间戳(8B) + offset(8B) + key长度 + key + value长度 + value
.index 文件(稀疏偏移量索引):
[offset(4B) + position(4B)] [offset(4B) + position(4B)] ...
每 4KB 日志数据生成一条索引(可配置: log.index.interval.bytes)
.timeindex 文件(时间戳索引):
[timestamp(8B) + offset(4B)] [timestamp(8B) + offset(4B)] ...
面试重点:
- 为什么不存全量索引?→ 稀疏索引更节约内存,通过二分查找 + 顺序扫描定位
- 为什么用
.index + .log 分离?→ 索引文件可载入内存加速定位(pagecache)
2.3 磁盘顺序写(核心设计亮点)
- 所有 Partition 文件顺序追加写(无随机写)
- 顺序写磁盘 ≈ 600MB/s,随机写 ≈ 100KB/s(差 6000 倍)
- 现代操作系统 Page Cache 机制进一步加速读写
2.4 零拷贝(Zero-Copy)技术
传统读取(4 次拷贝):
磁盘 → 内核页缓存 → 用户缓冲区 → Socket 内核缓存 → 网卡
零拷贝(sendfile,2 次拷贝):
磁盘 → 内核页缓存 → 网卡(DMA 直接传输)
- Kafka 使用
FileChannel.transferTo() / sendfile 系统调用
- 关键:消息消费时不走用户态,直接内核态读盘 → 网卡
- 减少上下文切换(2次 → 0次用户态介入)
2.5 页缓存(Page Cache)机制
- Kafka 不维护内存缓存,依赖操作系统 Page Cache
- 生产消息 → 写入 Page Cache → 异步刷盘(
flush.messages / flush.ms)
- 消费消息 → 从 Page Cache 读取 → 命中率极高
- 重启后 Page Cache 预热(Warm Up)需要时间 → 可用
vm.drop_caches 模拟测试
三、生产者原理(重点)
3.1 生产流程
Producer → Serializer → Partitioner → Accumulator → Sender 线程 → Network Client → Broker
- 序列化:Key/Value 序列化
- 分区选择 :
partition 指定 → key 哈希 → 轮询(默认 DefaultPartitioner:无 key 轮询,有 key 按 key 哈希)
- 消息累积(Batch) :消息写入
RecordAccumulator(每个 Partition 一个双端队列 Deque)
- Sender 线程 :后台线程不断拉取已准备好的 Batch,封装为
ProduceRequest
- 网络发送 :
NetworkClient 通过 Selector(NIO)发送请求,接收响应
3.2 关键参数
| 参数 |
默认值 |
说明 |
acks |
1 |
0=不等待, 1=Leader 写入, -1/all=ISR 全部写入 |
batch.size |
16KB |
消息批次大小,调大提升吞吐 |
linger.ms |
0 |
消息在 Buffer 等待时间,>0 可增加 Batch 大小 |
buffer.memory |
32MB |
Producer 端缓冲区大小 |
max.request.size |
1MB |
单次请求最大大小 |
compression.type |
none |
压缩: gzip/snappy/lz4/zstd |
retries |
INT_MAX |
重试次数(delivery.timeout.ms 控制总超时) |
3.3 ack 语义与一致性
| acks |
可靠性 |
Latency |
场景 |
| 0 |
可能丢(防火墙、网络问题) |
最低 |
日志、高吞吐容忍丢失 |
| 1 |
不丢(Leader 确认即返回) |
中 |
一般业务场景 |
| -1/all |
最强(ISR 全部写入确认) |
最高 |
金融、对账 |
注意 :acks=-1 时如果 min.insync.replicas 配合不当,写入可能全部不可用
四、消费者原理(高频)
4.1 推 vs 拉(面试重点)
| 模式 |
Kafka |
RabbitMQ |
| 方式 |
拉模式(Pull) |
推模式(Push)为主 |
| 优势 |
消费者自主控制消费速度 |
Broker 主动推送,延迟低 |
| 劣势 |
空轮询(空耗 CPU) |
消费者可能被压垮 |
Kafka 为什么用 Pull(面试必问):
- 避免 Consumer 被压垮(Push 模式下 Broker 不知道 Consumer 能力)
- 支持批量消费(吞吐优势)
- Consumer 可以自主重试、回溯
4.2 消费组与 Rebalance(高频难点)
消费组协调流程
1. 消费者启动 → 向 GroupCoordinator 发送 JoinGroup 请求
2. GroupCoordinator 选举 Leader(第一个加入的消费者)
3. Leader 制定分区分配方案 → 发送 SyncGroup 到 Coordinator
4. Coordinator 下发 SyncGroup 结果给所有消费者
5. 各消费者根据分配结果开始消费
Rebalance 触发条件
- 消费者加入 / 离开 / 崩溃(心跳超时)
- Topic 分区数变更
- 订阅 Topic 变更
Rebalance 协议
| 协议 |
版本 |
机制 |
影响 |
| Eager Rebalance |
旧版 |
停止全部消费 → 重新分配 → 恢复 |
影响大(Stop The World) |
| Cooperative Rebalance |
新版 (2.4+) |
逐步重新分配,部分 Consumer 不受影响 |
平滑、推荐 |
关键参数
| 参数 |
默认 |
说明 |
session.timeout.ms |
45000 |
消费者心跳超时 → 判定死亡 |
heartbeat.interval.ms |
3000 |
心跳间隔(应为 session.timeout 的 1/3) |
max.poll.interval.ms |
300000 |
两次 poll 最大间隔 → 超时则离开组 |
max.poll.records |
500 |
单次 poll 最多返回条数 |
4.3 Offset 提交
// 自动提交(默认)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true); // 默认 true
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
// 手动提交(推荐)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 同步提交
consumer.commitSync(); // 阻塞,重试直到成功
// 异步提交
consumer.commitAsync(); // 不阻塞,可能丢,需回调处理
重复消费原因:
- 自动提交间隔内宕机
- 手动提交失败后重试
- Rebalance 导致未提交 Offset 的分区重新分配
解决方案:幂等消费(业务幂等 / Offset 结合事务)
五、副本与高可用(核心难点)
5.1 ISR(In-Sync Replicas)
Partition
├── Leader ← 读写入口
├── Follower1 ← 从 Leader 同步(ISR 内)
├── Follower2 ← 从 Leader 同步(ISR 内)
└── Follower3 ← 同步落后(OSR / 非 ISR)
- ISR = 与 Leader 保持同步的副本集合
- 同步判定 :
replica.lag.time.max.ms(默认 30s)内未同步 → 踢出 ISR
- OSR (Out-of-Sync Replicas) = 落后副本
5.2 Leader 选举
Leader 宕机
↓
Controller 检测到 znode 变更 / 元数据变更
↓
Controller 从 ISR 中选举新 Leader(第一个同步的 Follower)
↓
新 Leader 开始服务读写
↓
ISR 缩小(旧 Leader 恢复后重新追赶)
- 选举策略 :优先从 ISR 选(数据不丢)→ 若 ISR 为空 +
unclean.leader.election.enable=true → 从 OSR 选(丢数据但可用)
- unclean.leader.election.enable :
false(默认)→ 保守,数据安全优先;true → 激进,可用性优先
5.3 Ack 时序详解
acks=-1 + min.insync.replicas=2 + replication.factor=3
Producer Leader Follower1 Follower2
| | | |
|-- ProduceRequest ------>| | |
| |--- Append ---->|-> ack |
| |--- Append ----------------->|-> ack
| |-- ISR 确认 | |
|<-- ProduceResponse -----| | |
- Leader 写入本地 log 后,等待所有 ISR 副本确认
min.insync.replicas 控制最小同步副本数(安全阈值)
- 若 ISR 数量 <
min.insync.replicas → Producer 收到 NotEnoughReplicasException
5.4 日志同步协议(Leader Epoch)
- Leader Epoch:每任 Leader 的任期号
- Epoch Entry :
(epoch, startOffset) 记录在 leader-epoch-checkpoint 文件中
- 作用 :
- 新 Leader 截断过期数据(Follower 可能多写了一些未提交数据)
- Follower 重启后通过 Epoch 找到正确的截断点(Truncation Point)
- 防止脑裂后的数据不一致(相比 ZooKeeper 的 zxid 更安全)
六、KRaft 模式(新)
6.1 去 ZooKeeper(Kafka 2.8+,3.x 正式 GA)
| 版本 |
元数据管理 |
| 2.x |
ZooKeeper 存储元数据 + Controller 选举 |
| 3.x (KRaft) |
自建 Raft 共识(Kafka 内部 QuorumController) |
6.2 KRaft 架构
Metadata Topic (@num.partitions=1, replication=N)
├── Partition-0 → Controller1 (Active Controller = Leader)
├── Partition-0 → Controller2 (Voter)
└── Partition-0 → Controller3 (Voter)
- Active Controller:处理所有元数据变更(创建 Topic、分区变更等)
- Metadata Topic:元数据变更日志(Raft Write-Ahead Log)
- Voter:参与 Raft 共识投票的节点(通常 3 或 5 个)
6.3 KRaft vs ZK 对比
| 维度 |
ZooKeeper 模式 |
KRaft 模式 |
| 外部依赖 |
需要 ZK 集群 |
无外部依赖 |
| Controller 选举 |
ZK 选主 |
Raft 选主 |
| 元数据存储 |
ZK 树形节点 |
Kafka Log(Topic) |
| 集群规模 |
较大(Controller 是单点瓶颈) |
更好(元数据可分区) |
| 部署复杂度 |
高(需独立 ZK) |
低 |
七、事务与幂等(进阶难点)
7.1 幂等 Producer
enable.idempotence=true # 默认 false,需同时设置 acks=all + retries>0
- 原理 :每个 Producer 分配唯一
producerId (PID),每条消息带 sequence number (seq)
- Broker 端按
(PID, Partition, seq) 去重 → 重复 seq 直接返回成功
7.2 事务性消息
producer.initTransactions();
producer.beginTransaction();
producer.send(record1);
producer.send(record2);
producer.commitTransaction(); // 或 abortTransaction()
- 事务协调器(Transaction Coordinator):处理事务状态
- 事务日志(Transaction Log) :存储事务状态(
__transaction_state Topic)
- 原子写入:多个 Partition 的写入要么全成功要么全失败
- 屏障(Barrier) :Consumer 配置
isolation.level=read_committed 才能读取已提交消息
7.3 Exactly-Once Semantics (EOS)
幂等 Producer + 事务 + 原子写入
- Kafka 保证 生产端 exactly-once(幂等去重)
- Kafka 保证 生产+消费 exactly-once(事务性幂等消费:Consumer offset 和 Producer 写入在同一个事务内)
- 流处理 EOS:Kafka Streams 在 source → process → sink 全链路保证 exactly-once
八、性能优化
8.1 生产者优化
| 手段 |
说明 |
增大 batch.size |
减少网络请求次数 |
增大 linger.ms |
增加 Batch 填充率(少量延迟换吞吐) |
| 开启压缩 |
compression.type=snappy(CPU 换带宽) |
调大 buffer.memory |
防止频繁阻塞 |
8.2 消费者优化
| 手段 |
说明 |
| 增加 Partition |
提升并行度(Partition 数 ≥ Consumer 数) |
增大 fetch.max.bytes |
增加每次拉取的数据量 |
增大 max.poll.records |
减少 poll 次数,提升批处理效率 |
| 处理逻辑异步化 |
消息放入队列,批量写入 DB |
8.3 Broker 优化
| 手段 |
说明 |
num.network.threads |
网络线程数(默认 3,大机器可调大) |
num.io.threads |
IO 线程数(默认 8,磁盘密集型调大) |
log.segment.bytes |
分段大小(默认 1GB,调小便于过期回收) |
log.retention.bytes |
日志保留大小 |
| 页缓存预分配 |
写入 > 2GB 避免 Page Cache 抖动 |
8.4 常见性能瓶颈
CPU:压缩/解压(可升级 CPU 或换压缩算法)
内存:Page Cache 不足 → 频繁磁盘 IO
磁盘:IOPS 不足 → 增加磁盘数 / 用 SSD
网络:带宽上限 → 压缩 + 多个网卡绑定
九、与其他 MQ 对比
| 特性 |
Kafka |
RabbitMQ |
RocketMQ |
| 协议 |
自定义 (TCP/NIO) |
AMQP |
自定义 |
| 吞吐量 |
百万级 |
万级 |
十万级 |
| 延迟 |
毫秒级 |
微秒级 |
毫秒级 |
| 消费模型 |
Pull(拉) |
Push(推) |
Pull(拉) |
| 消息顺序 |
Partition 内有序 |
单队列有序 |
Queue 内有序 |
| 消息堆积 |
极强(磁盘顺序写 + PageCache) |
一般(受限于内存) |
强 |
| Exactly Once |
支持(事务 + 幂等) |
不支持 |
支持 |
| 路由能力 |
弱(仅 Topic) |
强(多种 Exchange) |
中(Tag + SQL92) |
| 适用场景 |
大数据、日志、流处理、消息管道 |
企业级业务解耦 |
金融级、业务消息 |
十、高频面试题
10.1 Kafka 为什么这么快?
- 磁盘顺序写(利用磁盘顺序 IO 速度 ≈ 内存随机读)
- 零拷贝(sendfile) → 跳过用户态拷贝
- 页缓存(Page Cache) → 读写都走操作系统内核缓存
- 批量处理 → 消息累积为 Batch,减少网络 IO 次数
- 分区并行 → 多分区多消费者并行消费
- 稀疏索引 → 索引文件小,可常驻内存
10.2 消息丢失场景
| 场景 |
原因 |
解决 |
| 生产者丢失 |
acks=0 或网络异常 |
acks=-1 + 重试 |
| Broker 丢失 |
副本数=1 或 ISR 全挂 |
replication.factor>=3 + min.insync.replicas>=2 |
| 消费者丢失 |
自动提交未处理完 |
手动提交 + 幂等 |
10.3 消息重复场景
| 场景 |
原因 |
| Producer 重试 |
retries>0 + Broker 已写入但 ACK 丢失 |
| Rebalance |
处理中 Rebalance,未提交 Offset |
| 消费者宕机 |
手动提交失败 |
10.4 消息顺序保证
- 方案:同一个业务 key → 发送到同一个 Partition
- 限制:Partition 内有序,Partition 间无序
- 注意 :重试可能导致后发的消息先到 → 需
max.in.flight.requests.per.connection=1(牺牲吞吐)
10.5 消息堆积处理
- 增加消费者 + 增加 Partition(需要考虑 Reshard)
- 临时扩容:新建 Topic + 更多 Partition → 消费者先消费到临时 Topic → 再回流
- 排查 :消费者
max.poll.interval.ms 超时 → 挂掉 → 触发 Rebalance → 更慢
10.6 数据倾斜
- 症状:部分 Partition 消息特别多,部分很少
- 原因:Partition key 分布不均匀
- 解决:加盐(salted key)+ 二次分发
10.7 Kafka 为什么用 Pull 不用 Push?
- Push 模式下 Broker 可能压垮 Consumer(无法感知消费能力)
- Pull 模式下 Consumer 自主控制速率
- Pull 更适合批量消费(高吞吐)
- Pull 支持 Offset 重设(回溯消费)
10.8 Kafka 的存储为什么不用内存而用磁盘?
- 磁盘顺序写 ≈ 600MB/s,远超大部分业务需求
- 堆内存受 GC 影响(大对象 GC 开销大)
- 操作系统 Page Cache 很大(可用所有空闲内存)
- 重启后持久化数据不丢,内存缓存需要重新预热
10.9 为什么 Partition 数不能无限大?
- 每个 Partition 对应一个 Log 目录,大量文件导致文件系统压力
- Controller 管理元数据开销大(
/brokers/topics/ → Partition 越多 ZK 节点越多)
- Rebalance 时间随 Partition 增加而增加
- 选举和副本同步开销增大
十一、Spring Kafka 集成
11.1 核心配置
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
acks: -1
retries: 3
batch-size: 16384
compression-type: snappy
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: my-group
enable-auto-commit: false
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
ack-mode: manual
11.2 关键注解
| 注解 |
用途 |
@KafkaListener |
声明监听 Topic |
@KafkaHandler |
方法级别消息分发 |
@EnableKafka |
启用 Kafka 注解模式 |
11.3 编程式监听
@KafkaListener(topics = "orders", groupId = "order-group")
public void onMessage(
ConsumerRecord<String, String> record,
Acknowledgment ack) {
try {
// 业务处理
ack.acknowledge();
} catch (Exception e) {
// 记录到死信或重试
}
}
十二、总结记忆口诀
一 Topic 多分区,写入只追加 磁盘顺序写 PageCache,零拷贝 sendfile ISR 保数据,acks 控可靠 Pull 模式自主控吞吐,Rebalance 触发全停止 幂等 PID+seq,事务原子跨分区 KRaft 去 ZK,Raft 统一元数据