消息队列重点详解

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)
java 复制代码
channel.txSelect();
channel.basicPublish(...);
channel.txCommit();    // 或 txRollback()
  • 缺点:同步阻塞,吞吐量极低(约下降 250 倍)
  • 面试点:知道有事务但实际不用,改用 Confirm
2) 发送方确认(Publisher Confirm)
  • 普通 Confirm:每发一条 waitForConfirms()
  • 批量 Confirm:每 N 条 waitForConfirmsOrDie()
  • 异步 Confirm:添加 ConfirmListener,回调处理 ack/nack(性能最优,推荐)
java 复制代码
channel.confirmSelect();
// 异步监听
channel.addConfirmListener((deliveryTag, multiple) -> {
    // ack 回调
}, (deliveryTag, multiple) -> {
    // nack 回调
});

2.2 消息可靠存储

  • 持久化三要素(缺一不可)

    1. Exchange 持久化:channel.exchangeDeclare(..., true)
    2. Queue 持久化:channel.queueDeclare(..., true, ...)
    3. Message 持久化:basicPublish(..., MessageProperties.PERSISTENT_TEXT_PLAIN)
  • 注意 :持久化消息也要写入磁盘后才算真正持久,可使用 queueDeclaredurable=true + deliveryMode=2

2.3 消费者可靠性

ACK 机制
类型 说明 是否会丢弃
自动 ACK 消费者收到即确认 可能丢消息(consumer 宕机)
手动 ACK 处理完业务才确认 不丢,但需处理重复
否定 ACK (nack) 拒绝消息 依赖 requeue 参数
java 复制代码
// 手动确认
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)
java 复制代码
channel.basicQos(1);  // 每次只发1条,处理完再发下一条
  • 关键:避免不公平分发,防止消费者堆积

2.4 幂等性问题(必问)

  • RabbitMQ 的 at-least-once 语义 → 消费者需自行保证幂等
  • 方案:消息 ID 去重表、业务主键判重、Redis Set 去重

三、高级特性与难点

3.1 死信队列(DLQ)

死信来源

  1. 消息被拒绝并 requeue=false
  2. 消息 TTL 过期
  3. 队列达到最大长度

配置方式

java 复制代码
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 优先级队列

java 复制代码
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 镜像队列配置

bash 复制代码
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 性能瓶颈

  1. 磁盘 IO:持久化消息的写入瓶颈
  2. 内存:消息堆积导致内存暴涨
  3. 网络:集群间复制延迟

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 核心配置

yaml 复制代码
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 生产者发送阶段

scss 复制代码
Producer → Channel → Exchange → Binding匹配 → Queue(s)
  1. 建立连接 :Producer 通过 TCP 连接到 RabbitMQ Node(默认 5672 端口),在 TCP 连接上创建 Channel(轻量级多路复用,一个 Connection 可开多个 Channel)
  2. 声明 Exchange/Queuechannel.exchangeDeclare() / channel.queueDeclare() --- 元数据写入 Mnesia(RabbitMQ 内嵌数据库)
  3. 发送消息channel.basicPublish(exchange, routingKey, props, body)
  4. Exchange 路由:根据 Exchange 类型 + Binding 规则,将消息路由到 0/N 个队列
  5. Mandatory 回退 :若路由不到任何队列 + mandatory=true → 触发 basic.return;否则消息静默丢弃
  6. 持久化:若消息标记为持久化 → 写入磁盘(Queue Index + Message Store)
  7. Confirm 回调 :若开启 Confirm 模式 → 消息写入磁盘/路由完成后 → Broker 回送 basic.ack

9.2 Broker 内部流转

css 复制代码
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
  1. 推模式(Push)basic.consume 注册 Consumer,Broker 主动推送消息(最常用)
  2. 拉模式(Pull)basic.get 消费者主动拉取(轮询,效率低,不推荐)
  3. QoS 控制:根据 Prefetch Count 决定一次推送多少条未确认消息
  4. 消息投递 :Broker 从 Queue 的 ready 队列头部取出消息,封装为 basic.deliver 帧发送给 Consumer
  5. ACK 处理
    • 收到 basic.ack → 从 unacked 列表移除消息
    • 收到 basic.nack + requeue=true → 重新入队 ready 列表头部或尾部
    • Channel 关闭 / 连接断开 → 所有 unacked 消息自动重新入队(at-least-once 语义的来源)

9.4 完整时序图

lua 复制代码
Producer                  Exchange                  Queue                  Consumer
   |                          |                       |                       |
   |-- basic.publish -------->|                       |                       |
   |                          |-- routing ----------->|                       |
   |                          |                       |-- basic.deliver ----->|
   |                          |                       |                       |-- 业务处理
   |                          |                       |<-- basic.ack ---------|
   |-- basic.ack (Confirm) -->|                       |                       |

十、底层实现原理

10.1 AMQP 协议帧结构(Wire Level)

RabbitMQ 基于 AMQP 0-9-1 协议,所有通信均为二进制帧:

scss 复制代码
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 消息存储引擎

分层存储架构
lua 复制代码
                  +-------------------+
                  |  Queue Index      |  → 消息逻辑位置(seq_id → 文件偏移)
                  +-------------------+
                          |
                  +-------------------+
                  |  Message Store    |  → 消息体二进制数据(文件追加写)
                  +-------------------+
                          |
                  +-------------------+
                  |  文件系统 (NTFS)  |
                  +-------------------+
消息写磁盘流程(持久化消息)
markdown 复制代码
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 磁盘 消息体在磁盘,无进程引用(大量堆积时)

状态转换(面试高频):

scss 复制代码
        消息入队
            ↓
   ┌───  Alpha (内存)  ───┐
   │        ↓ 内存不足      │
   │     Beta (索引在内存)  │
   │        ↓               │
   │     Gamma (磁盘索引)   │
   │        ↓ 堆积严重      │
   └──  Delta (全磁盘) ────┘
  • 投递效率:Alpha > Beta > Gamma > Delta
  • 触发条件vm_memory_high_watermark(默认 40%)→ 触发换出(swap out)
  • 惰性队列:消息直接进入 Delta,跳过 Alpha/Beta

10.5 网络与 IO 线程模型

scss 复制代码
┌─────────────────────────────────┐
│    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 机制防止生产者淹没消费者:

lua 复制代码
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(消费者流控)不同
内存水位控制
bash 复制代码
# 默认配置
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)

lua 复制代码
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 共识算法
  • 写入过程
    1. Leader 接收消息 → 追加本地的 WAL (Write Ahead Log)
    2. 并行发送 AppendEntries RPC 到 Follower
    3. 多数派(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

vbnet 复制代码
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 磁盘文件布局

bash 复制代码
/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 结构

css 复制代码
.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)技术

markdown 复制代码
传统读取(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 生产流程

arduino 复制代码
Producer → Serializer → Partitioner → Accumulator → Sender 线程 → Network Client → Broker
  1. 序列化:Key/Value 序列化
  2. 分区选择partition 指定 → key 哈希 → 轮询(默认 DefaultPartitioner:无 key 轮询,有 key 按 key 哈希)
  3. 消息累积(Batch) :消息写入 RecordAccumulator(每个 Partition 一个双端队列 Deque)
  4. Sender 线程 :后台线程不断拉取已准备好的 Batch,封装为 ProduceRequest
  5. 网络发送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(面试必问):

  1. 避免 Consumer 被压垮(Push 模式下 Broker 不知道 Consumer 能力)
  2. 支持批量消费(吞吐优势)
  3. Consumer 可以自主重试、回溯

4.2 消费组与 Rebalance(高频难点)

消费组协调流程
markdown 复制代码
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 提交

java 复制代码
// 自动提交(默认)
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)

sql 复制代码
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 选举

markdown 复制代码
Leader 宕机
    ↓
Controller 检测到 znode 变更 / 元数据变更
    ↓
Controller 从 ISR 中选举新 Leader(第一个同步的 Follower)
    ↓
新 Leader 开始服务读写
    ↓
ISR 缩小(旧 Leader 恢复后重新追赶)
  • 选举策略 :优先从 ISR 选(数据不丢)→ 若 ISR 为空 + unclean.leader.election.enable=true → 从 OSR 选(丢数据但可用)
  • unclean.leader.election.enablefalse(默认)→ 保守,数据安全优先;true → 激进,可用性优先

5.3 Ack 时序详解

lua 复制代码
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 文件中
  • 作用
    1. 新 Leader 截断过期数据(Follower 可能多写了一些未提交数据)
    2. Follower 重启后通过 Epoch 找到正确的截断点(Truncation Point)
    3. 防止脑裂后的数据不一致(相比 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 架构

less 复制代码
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

properties 复制代码
enable.idempotence=true  # 默认 false,需同时设置 acks=all + retries>0
  • 原理 :每个 Producer 分配唯一 producerId (PID),每条消息带 sequence number (seq)
  • Broker 端按 (PID, Partition, seq) 去重 → 重复 seq 直接返回成功

7.2 事务性消息

java 复制代码
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 为什么这么快?

  1. 磁盘顺序写(利用磁盘顺序 IO 速度 ≈ 内存随机读)
  2. 零拷贝(sendfile) → 跳过用户态拷贝
  3. 页缓存(Page Cache) → 读写都走操作系统内核缓存
  4. 批量处理 → 消息累积为 Batch,减少网络 IO 次数
  5. 分区并行 → 多分区多消费者并行消费
  6. 稀疏索引 → 索引文件小,可常驻内存

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 核心配置

yaml 复制代码
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 编程式监听

java 复制代码
@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 统一元数据

相关推荐
RustCoder1 小时前
MangoFetch:一个用 Rust 写的 CLI/TUI 高性能的下载工具
后端·rust·开源
程序员清风1 小时前
AI开发岗该如何准备面试?
java·后端·面试
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第20篇:Java初始化、构造器、对象创建的过程
java·开发语言·后端·面试
Lee川2 小时前
从输入框到智能匹配:一文读懂搜索功能的完整实现
前端·后端
朝阳392 小时前
React【面试】
前端·react.js·面试
豹哥学前端3 小时前
前端 LocalStorage 实战:从入门到熟练,一篇就够了
前端·javascript·面试
传说之后3 小时前
深入浅出 Raft:万字解析分布式共识的核心设计
后端
小小小小宇3 小时前
Go 后端高并发架构:从外到内的立体防御体系
后端