Demo 目标
项目目录:
text
kafka-knowledge-system/demos/springboot-kafka-order-demo
这个 demo 模拟"订单服务发送订单事件,消费者处理并保存处理结果"的完整链路,覆盖生产项目最常见能力:
- REST API 接收订单。
- Producer 发送 Kafka 消息。
- 使用
orderId作为 key 保证同订单顺序。 - Consumer 手动确认 offset。
- 消费失败自动重试。
- 重试失败后进入 DLT。
- 消费端基于
eventId做幂等。 - 提供查询接口验证处理结果。
项目结构
text
springboot-kafka-order-demo
├── pom.xml
├── src/main/java/com/example/kafkaorder
│ ├── KafkaOrderDemoApplication.java
│ ├── config/KafkaConfig.java
│ ├── controller/OrderController.java
│ ├── event/OrderEvent.java
│ ├── producer/OrderEventProducer.java
│ ├── consumer/OrderEventConsumer.java
│ └── service/OrderEventStore.java
└── src/main/resources/application.yml
启动 Kafka
先启动 CLI demo 中的 Kafka:
bash
cd kafka-knowledge-system/demos/cli-kafka-lab
docker compose up -d
确认端口:
bash
docker compose ps
Kafka 对宿主机暴露 localhost:9094,Spring Boot demo 默认连接这个地址。
启动项目
bash
cd kafka-knowledge-system/demos/springboot-kafka-order-demo
mvn spring-boot:run
如果首次运行 Maven 需要下载依赖,请保持网络可用。
发送正常订单事件
bash
curl -X POST http://localhost:8080/orders \
-H 'Content-Type: application/json' \
-d '{"orderId":"O1001","userId":"U1","amount":99.80,"status":"CREATED"}'
预期返回:
json
{
"eventId": "....",
"eventType": "ORDER_CREATED",
"orderId": "O1001",
"userId": "U1",
"amount": 99.80,
"status": "CREATED"
}
查询处理结果:
bash
curl http://localhost:8080/orders/O1001
预期能看到订单事件已被消费者处理。
触发失败和 DLT
发送一个状态为 FAIL 的事件:
bash
curl -X POST http://localhost:8080/orders \
-H 'Content-Type: application/json' \
-d '{"orderId":"O9001","userId":"U9","amount":10.00,"status":"FAIL"}'
消费者会模拟业务异常,重试后进入 DLT:
bash
docker compose exec kafka kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic order-events.DLT \
--from-beginning \
--timeout-ms 5000
核心代码解释
Producer
OrderEventProducer 发送消息时使用:
java
kafkaTemplate.send("order-events", event.orderId(), event)
这里的 key 是 orderId,作用是让同一个订单的事件进入同一个 partition,保证单订单维度顺序。
Consumer
OrderEventConsumer 使用手动确认:
java
store.handle(event);
acknowledgment.acknowledge();
只有业务处理成功后才提交 offset。失败时抛出异常,交给 Spring Kafka 的错误处理器重试。
幂等
OrderEventStore 使用 eventId 记录已处理事件。如果同一事件重复投递,消费者直接跳过,避免重复执行业务逻辑。
生产环境可以把这个逻辑落到数据库唯一键:
sql
create table processed_event (
event_id varchar(64) primary key,
processed_at timestamp not null
);
处理流程:
text
开始数据库事务
插入 processed_event(event_id)
如果唯一键冲突,说明已处理,直接返回
执行业务更新
提交事务
提交 Kafka offset
验证清单
| 验证项 | 操作 | 预期 |
|---|---|---|
| Topic 自动创建 | 启动应用 | order-events 存在 |
| Producer 正常 | POST /orders |
返回事件 JSON |
| Consumer 正常 | GET /orders/{id} |
能查到处理结果 |
| Key 分区 | 多发同一 orderId | 同订单顺序处理 |
| 重试 | status=FAIL |
日志出现重试 |
| DLT | 消费最终失败 | order-events.DLT 有消息 |
| 幂等 | 重复 eventId | 不重复处理 |
项目扩展练习
- 增加订单支付事件
ORDER_PAID。 - 把内存存储替换为 MySQL。
- 使用 Outbox 表保证数据库订单和 Kafka 事件最终一致。
- 增加消息 Schema 版本字段兼容处理。
- 增加 Prometheus 指标:发送成功数、消费成功数、DLT 数、处理耗时。
- 增加 Testcontainers 集成测试。
专家级能力模型
Kafka 专家不是只会背 ISR、HW、ACK,而是能把业务目标、客户端配置、集群状态、监控指标和故障现场串起来。
| 能力 | 具体表现 |
|---|---|
| 架构设计 | 能设计 topic、partition、副本、Schema、权限和容量 |
| 可靠性治理 | 能解释丢失、重复、乱序,并给出业务幂等方案 |
| 性能治理 | 能从 producer、broker、consumer 三端定位瓶颈 |
| 运维治理 | 能扩容、迁移、升级、故障演练 |
| 排障能力 | 能通过 lag、ISR、请求延迟、磁盘指标定位问题 |
| 源码理解 | 能理解 Controller、Coordinator、Replica、Log、Network |
源码阅读路线
Kafka 源码可以按链路阅读,不建议从入口文件盲读。
第一阶段:请求链路
目标:理解 producer 请求到 broker 后怎么处理。
重点模块:
SocketServerKafkaRequestHandlerKafkaApisReplicaManagerPartitionUnifiedLog
阅读问题:
- ProduceRequest 如何进入 broker?
- broker 如何找到 topic partition leader?
- 消息如何追加到日志?
- 什么时候返回成功?
第二阶段:复制链路
重点模块:
ReplicaFetcherThreadReplicaManagerPartition- ISR 管理逻辑
阅读问题:
- follower 如何从 leader 拉取数据?
- ISR 什么时候扩大或缩小?
- HW 如何推进?
- leader 宕机后如何保证消费者不读到不稳定数据?
第三阶段:消费组
重点模块:
GroupCoordinatorConsumerCoordinatorAbstractCoordinator- assignor 实现
阅读问题:
- 消费者如何加入 group?
- rebalance 如何触发?
- offset 存在哪里?
- static membership 如何减少 rebalance?
第四阶段:Controller 和元数据
重点模块:
- KRaft Controller
- metadata log
- partition leader election
阅读问题:
- KRaft 如何替代 ZooKeeper?
- controller 负责哪些元数据变更?
- leader election 如何写入元数据日志?
- broker 如何感知元数据变化?
完整面试题与答案
1. Kafka 为什么吞吐高?
Kafka 高吞吐来自多个设计叠加:磁盘顺序追加写、OS page cache、批量发送、压缩、零拷贝、partition 并行、broker 横向扩展。顺序写避免随机 IO;page cache 让热点数据读写走内存;producer 将多条消息合批降低网络请求数;消费者批量拉取;partition 分布在多个 broker 上提高并行度。
追问:Kafka 是不是完全不依赖磁盘?
不是。Kafka 数据最终存储在磁盘日志中,只是大量读写会被 page cache 吸收。磁盘性能仍然决定长期稳定吞吐和恢复速度。
2. Topic、Partition、Offset 的关系是什么?
Topic 是消息分类,partition 是 topic 的物理分片,offset 是消息在某个 partition 内的递增位置。Kafka 只保证同一个 partition 内消息有序,不保证 topic 全局有序。消费者提交的是某个消费组在某个 topic-partition 上的 offset。
3. 为什么同一消费组内,一个 partition 不能被多个消费者同时消费?
因为 Kafka 用 partition 作为并行和顺序边界。如果同一个 partition 同时分给多个消费者,就无法保证 offset 提交和顺序处理的一致性。同一消费组内可以多个 partition 分给多个消费者,但一个 partition 同一时刻只分配给一个消费者。
4. Kafka 如何保证消息不丢?
需要三端配合:
- Producer:
acks=all、enable.idempotence=true、合理重试。 - Broker:核心 topic 使用 3 副本、
min.insync.replicas=2、禁用 unclean leader election。 - Consumer:业务处理成功后再提交 offset。
还要接受一个现实:业务端通常需要幂等,因为网络超时和重试会造成重复投递。
5. Kafka 会不会重复消费?
会。Kafka 常见语义是至少一次投递。比如消费者业务处理成功后,提交 offset 前宕机,重启后会重新消费同一条消息。解决方式是在业务层用 eventId、业务唯一键、去重表或状态机做幂等。
6. 如何保证消息顺序?
把需要保证顺序的消息用同一个 key 发送到同一个 partition。例如同一订单的事件用 orderId 作为 key。Kafka 保证 partition 内顺序。消费者处理时也要避免对同一 partition 内消息并发乱序处理。
边界:如果需要全局顺序,只能使用一个 partition,但吞吐和并行度会下降。
7. ISR 是什么?
ISR 是 In-Sync Replicas,表示与 leader 保持同步的副本集合。Producer 使用 acks=all 时,leader 需要等待 ISR 中满足条件的副本确认后才返回成功。ISR 缩小说明副本复制跟不上,可能由 broker 宕机、网络抖动、磁盘慢或 GC 引起。
8. HW 和 LEO 的区别是什么?
LEO 是某个副本日志末尾 offset,代表该副本写到了哪里。HW 是消费者可见的最高安全 offset,代表 ISR 副本都确认到的位置。消费者只能读取 HW 之前的数据,避免读到 leader 写入但尚未复制、后来可能丢失的数据。
9. acks=all 是否一定不丢消息?
不是。acks=all 需要配合副本和 ISR 配置才有意义。如果 topic 只有一个副本,acks=all 也只是等一个副本。生产环境应使用 replication.factor=3、min.insync.replicas=2,并禁用 unclean leader election。
10. 什么是 unclean leader election?
当 ISR 中没有可用副本时,Kafka 是否允许一个落后副本成为 leader。允许时可提高可用性,但可能丢失已确认消息;禁止时宁可 partition 暂时不可用,也不让落后副本成为 leader。核心业务通常禁用。
11. Consumer Lag 很高怎么排查?
先判断 lag 是持续增长还是短暂波峰。持续增长时按顺序排查:消费者是否报错、处理耗时是否增加、下游是否慢、消费者实例数是否不足、partition 是否不足、是否频繁 rebalance、是否某个 partition 热点。临时方案是扩容消费者、降低批处理耗时、启用 DLT、暂停非核心消费。
12. Rebalance 频繁的原因是什么?
常见原因:消费者处理时间超过 max.poll.interval.ms、实例频繁发布或崩溃、心跳超时、topic partition 变化、消费组成员不稳定。治理方式:优化处理耗时、合理调大 max.poll.interval.ms、使用 cooperative sticky assignor、静态成员 ID、优雅停机。
13. Kafka 事务解决什么问题?
Kafka 事务解决 Kafka 内部多条消息写入和 offset 提交的原子性。例如从 topic A 消费,处理后写 topic B,同时提交 A 的 offset,要么都成功,要么都不可见。它不能直接保证数据库和 Kafka 的跨系统强一致。
14. 数据库更新成功但 Kafka 发送失败怎么办?
推荐 Outbox 模式。业务事务中同时写业务表和 outbox_event 表。事务提交后,由后台任务或 CDC 读取 outbox 表发送 Kafka。发送成功后标记状态。这样避免"数据库成功但消息丢失"的不一致。
15. Kafka 如何做容量规划?
先收集峰值 TPS、平均消息大小、保留时间、副本数、压缩比、目标磁盘利用率。存储公式:
text
TPS * 消息大小 * 86400 * 保留天数 * 副本数 * 压缩比 / 磁盘目标利用率
再根据目标吞吐和消费者并行度估算 partition 数,并预留增长空间。
16. Partition 可以随便增加吗?
可以增加,但不能减少。增加 partition 会改变 key 到 partition 的映射,可能影响按 key 的顺序假设。增加过多还会增加文件句柄、元数据、controller、rebalance 和恢复成本。
17. Kafka 和 RabbitMQ 怎么选?
Kafka 更适合高吞吐、事件流、可回放、多订阅、日志采集和流计算。RabbitMQ 更适合传统任务队列、复杂路由、低到中等吞吐、每条消息投递语义更强的场景。选型要看业务是否需要保留事件流和回放能力。
18. 如何设计 Kafka 消费端幂等?
常见方式:
- 事件表唯一键:
event_id作为主键。 - 业务状态机:订单已支付就不重复支付。
- Redis set 去重:适合短期去重,但要处理过期和一致性。
- 数据库唯一约束:最稳妥,适合核心业务。
核心原则:先判断是否处理过,再执行业务更新,并把判断和业务更新放在同一个事务边界内。
19. 如何处理毒丸消息?
毒丸消息是永远无法成功处理的消息,例如字段格式错误、业务状态非法。不能无限重试阻塞 partition。应该有限次重试,失败后进入 DLT,同时记录原因、topic、partition、offset、key、异常堆栈,后续人工修复或补偿。
20. Kafka 专家排障时最先看什么?
先看影响面:是 producer 写不进、consumer 堆积、broker 不健康,还是某个 topic 异常。然后看四组指标:consumer lag、under replicated partitions、request latency、磁盘/网络/CPU。不要只看应用日志,Kafka 问题通常需要客户端、broker 和下游系统一起看。