文章目录
- 前言
- 一、先画地图:一件消息包裹的完整旅程
- 二、第一道坎:消息会不会丢?
-
- [2.1 生产端:寄件的时候怎么防丢?](#2.1 生产端:寄件的时候怎么防丢?)
- [2.2 存储端:仓库里怎么防丢?](#2.2 存储端:仓库里怎么防丢?)
- [2.3 消费端:签收的时候怎么防丢?](#2.3 消费端:签收的时候怎么防丢?)
- 三、第二道坎:消息会不会重复?
- 四、第三道坎:消息顺序怎么保证?
- 五、实战:消息堆积了,怎么定位、怎么清?
- 六、高频面试题速答:把原理串成话术
- 结语
前言
每个 Java 开发者心里都有个"Kafka 情结":一听就高大上,一用就心里没底。它就像一家口碑两极分化的快递公司------配置得当,包裹一个不落、飞快送达;配置一错,丢件、重派、乱序三连击,能把你的线上系统折腾到半夜。
这篇文章不带你把 Kafka 的 API 从头背一遍,而是跟着一件"消息包裹",从寄件人手里一路跟到收件人签收,看它在路上的每一道坎是怎么过的:丢件怎么防、重复怎么挡、顺序怎么保、堆积怎么清。
这一路走下来,你手里会多出两样法宝------一张 Kafka 全链路的地图,和一套能应对 90% Kafka 面试题的回答逻辑。
一、先画地图:一件消息包裹的完整旅程
Kafka 的世界里只有四类角色,一句话就能记住:Producer 寄件,Broker 转运,Topic 入库,Consumer 签收。
-
• Producer(生产者):寄件人,负责把消息发出去
-
• Broker(代理):快递公司的中转中心,一个集群里通常有多个 Broker
-
• Topic(主题):收件总仓,一类消息对应一个 Topic
-
• Partition(分区) :Topic 里的分拣口,一个 Topic 可以拆成多个分区,每个分区内的消息有顺序
-
• Consumer(消费者):收件人,订阅 Topic 拉取消息;多个消费者组成 Consumer Group,分摊不同分区的消息

Note
关键先记住一点: 分区是 Kafka 最小的"有序单元",顺序保证是以分区为边界的。这个设定,是后面所有"不丢、不重、不乱"故事的根基。
这一圈走下来,你已经发现了:一件包裹从寄出到签收,要经过生产端、存储端、消费端 三个环节。而 Kafka 所有的高频面试题,本质都在问同一件事------包裹在这三个环节里,怎么不出幺蛾子?
接下来我们一件坎一件坎地过。第一道坎,也是最要命的:消息会不会丢?
二、第一道坎:消息会不会丢?
"Kafka 会丢消息吗?"这可能是面试里出现率最高的 Kafka 问题。直接说结论:
不丢的前提
只要你在三个环节都做了正确的配置,生产环境里 Kafka 几乎不会丢消息------丢,通常是因为你只防了其中一环。
逐环来看。
2.1 生产端:寄件的时候怎么防丢?
Producer 发消息时,可以要求 Broker 给一个"回执",回执的严格程度由 acks 参数控制:
| acks 取值 | 含义 | 丢消息风险 |
|---|---|---|
acks=0 |
发完不管,不等确认 | 高,消息可能直接丢 |
acks=1 |
Leader 写入本地就确认 | 中,Leader 挂了可能丢 |
acks=all |
所有 ISR 副本都写入才确认 | 低,最安全 |
// 生产环境最稳的写法:等所有副本确认 + 失败自动重试 Properties props = new Properties(); props.put("bootstrap.servers", "kafka-1:9092,kafka-2:9092"); props.put("acks", "all"); // 所有副本确认才算成功 props.put("retries", Integer.MAX_VALUE); // 失败无限重试
重试会乱序
你发消息 A 失败后重试,可消息 B 已经发出去了,等 A 重试成功,顺序就变成了 B→A。这个坑留到第四节专门讲------先在心里记下"重试会乱序"这笔账。
2.2 存储端:仓库里怎么防丢?
寄出去的包裹到了 Broker,如果 Broker 的机器突然宕机,数据怎么办?Kafka 的答案是:副本(replica)------同一份数据,多台机器各存一份。
每个分区有一个 Leader 副本负责对外读写,若干个 Follower 副本跟在后面同步。Follower 里"跟得上的"会被拉进一个名单,叫 ISR(In-Sync Replicas,同步副本):

Note
Kafka 不丢消息的底气,来自副本------同一份数据,多个分拣员手里都有一份。 Leader 挂了,从 ISR 里选个 Follower 顶上,消息就还在。前提是
unclean.leader.election.enable保持默认false------一旦放开,允许从 ISR 之外选主,就可能选到数据不完整的副本当 Leader,已确认的消息也会丢。
配套的参数是 min.insync.replicas:它规定"至少几个副本同步成功,才算写入成功"。结合 acks=all 一起用,才能形成真正的"不丢"防线。
# Broker 配置 min.insync.replicas=2 # 至少 2 个副本确认写入
Warning
acks=all和min.insync.replicas必须配对使用,否则acks=all可能退化成只等 1 个副本,防丢防线就漏风了。
2.3 消费端:签收的时候怎么防丢?
包裹最后一道坎在消费者手里。消费者拉取消息后,要告诉 Broker"我处理完了,下次从这往后拉",这个记录叫 offset(偏移量)。
这里的坑,是"先提交 offset 还是先处理消息"的顺序问题:
-
• 先提交 offset,再处理消息 → 消息处理失败,offset 已往前跳 → 丢消息
-
• 先处理消息,再提交 offset → 消息处理完但 offset 没提交,消费者重启 → 重复消费
// 手动提交 offset:处理成功后再提交,避免处理失败却已跳位 consumer.subscribe(Collections.singletonList("order_topic")); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000)); for (ConsumerRecord<String, String> record : records) { handleOrder(record.value()); // 先处理业务 } consumer.commitSync(); // 处理成功后再提交 offset }
不丢与重复的分水岭
消费者的 offset 提交时机,是"丢消息"和"重复消息"的分水岭------想要不丢,就必须接受偶尔重复。
到这里,不丢的三道防线你已经全部拿到了:生产端 acks=all、存储端副本 + ISR、消费端先处理再提交。消息不丢,是 Kafka 设计得最扎实的能力,但代价马上就来------你会开始遭遇"重复"。
三、第二道坎:消息会不会重复?
快递员把同一个包裹派了两次,谁都遇到过。Kafka 也一样:消费者处理完消息但还没来得及提交 offset 就宕机了,重启后它会把这条消息再拉一次 。这就是重复消费,它是"不丢"的必然副产品。
业界把投递语义分成三档:
| 投递语义 | 说明 | Kafka 能保证吗 |
|---|---|---|
| 至少一次(at least once) | 消息可能重复,但不丢 | ✅ 默认就是 |
| 至多一次(at most once) | 消息可能丢,但不重复 | ✅ 先提交 offset 再处理 |
| 恰好一次(exactly once) | 不丢也不重 | ⚠️ 需要特殊配置 |
生产环境里,我们几乎都选"至少一次"------然后用幂等去消化重复。
怎么消化?两个层面:
第一层,生产者幂等 :开启 enable.idempotence=true,Kafka 会给每条消息打一个序列号,重复发送时自动去重。
props.put("enable.idempotence", "true"); // 生产者幂等,避免重复写入
第二层,消费者幂等(这才是重点) :即使上游不重复,下游处理也必须是"重复执行结果一样"的。最经典的做法是业务唯一键 + 去重 ------比如订单消息里带一个 orderId,去重时用"先占坑"(insert-if-not-exists,等价于数据库唯一索引)而不是"先查再标记",否则同一订单被并发处理时仍可能重复。
void handleOrder(OrderMessage msg) { // 先"占坑":orderId 已存在说明处理过,直接跳过(insert-if-not-exists 的原子语义) if (!dedupService.tryMark(msg.getOrderId())) { return; } orderService.create(msg); // 业务只执行一次 }
重复要靠你自己挡
Kafka 只能保证"不丢",重复要靠你自己挡------消费者幂等是每个 Kafka 项目里必须写的一道防御。
重复的问题搞定,你以为万事大吉了?错,第三道坎正等着你:消息顺序乱了。
四、第三道坎:消息顺序怎么保证?
同一个订单的"创建"和"支付"两条消息,如果顺序反了,业务就崩了。Kafka 对顺序的承诺是:
Kafka 对顺序的承诺
Kafka 只保证分区内有序------同一个分区里的消息,消费顺序和写入顺序一致(前提是消费者单线程处理该分区,多线程并发处理同一分区仍会乱序)。跨分区的消息,顺序不保证。
所以保证顺序的核心思路只有一个:把必须有序的消息,路由进同一个分区。
// 用业务 key 做分区路由:同一个 orderId 的所有消息进同一个分区 ProducerRecord<String, String> record = new ProducerRecord<>("order_topic", orderId, message); producer.send(record);
这正是 key 的用法:key 相同的消息,永远进入同一个分区,天然有序。 你的订单、支付、退款消息只要都带上 orderId 作为 key,顺序就稳了。
还有一个容易翻车的细节------还记得第一节记下的那笔账吗?重试会乱序。
// 重试 + 多连接并发发消息 → 可能乱序 props.put("max.in.flight.requests.per.connection", "5"); // 同一连接可并发 5 个未确认请求
如果 max.in.flight.requests.per.connection 大于 1,消息 A 重试期间,B 已经发出去了,回来就乱序。所以要保顺序,要么调成 1,要么开启幂等------幂等靠 PID + 序列号校验来保序:Broker 发现乱序批次会拒绝并让 Producer 重发,即使 in-flight 保持 5 也不会乱序,还不用牺牲吞吐。
| 保顺序手段 | 代价 | 适用场景 |
|---|---|---|
| key 路由进同一分区 | 分区数据可能倾斜 | 绝大多数业务,首选 |
开启幂等 enable.idempotence=true |
几乎无额外代价,in-flight 保持 5 也不乱序 | 需要保序,推荐首选 |
max.in.flight.requests.per.connection=1 |
吞吐下降 | 顺序敏感且没法开幂等 |
| 单分区 | 吞吐被单分区卡死 | 几乎不推荐 |
到这里,不丢、不重、不乱的原理都过完了。但理论终究是理论,真正逼疯开发者的,永远是线上那个只涨不降的"堆积"数字。
五、实战:消息堆积了,怎么定位、怎么清?
"消费 lag 一直涨,怎么处理?"------这是 Kafka 面试的压轴题,也是线上最真实的疼。
消息堆积的本质:生产速度 > 消费速度,消费者追不上生产者。 绝大多数时候,锅不在 Kafka,而在消费者。

排查三板斧,由便宜到贵依次上:
-
- 先定位瓶颈 :用
kafka-consumer-groups.sh看每个分区的 lag,确认是哪个分区、哪个消费者卡住了
- 先定位瓶颈 :用
-
- 再优化消费逻辑:最常见的是消费者里同步查数据库、同步调远程接口------改成异步化、批量处理、加本地缓存,往往立竿见影
-
- 最后才扩容 :增加消费者数量的前提是分区数要够(同一消费组内一个分区同一时刻只能被一个消费者处理),所以扩容消费者通常要先扩分区,这是大家最容易踩的坑
# 查看消费组每个分区的 lag,定位堆积源头 kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 \ --group order_consumer_group --describe
先查自己,再动集群
消息堆积 90% 不是 Kafka 的锅,是消费者的锅------先查自己,再动集群。
到这里,实战的坑讲完了。这些原理都滚瓜烂熟了,可真的坐在面试官对面,你该怎么把这些话有条理地说出来?这就是最后一节要解决的。
六、高频面试题速答:把原理串成话术
面试官考 Kafka,从来不是考你记了多少概念,而是看你能不能用一条逻辑主线把知识串起来。下面这套回答,就是前面所有内容的"串讲版",面试被问到可以直接背:
Q1:Kafka 怎么保证消息不丢失?
从三个环节答。生产端设
acks=all,等所有 ISR 副本确认才返回成功,失败自动重试;存储端靠副本机制,每个分区有 Leader 和多个 Follower,Leader 挂了从 ISR 里选新的,配合min.insync.replicas=2保证至少两个副本同步;消费端用手动提交 offset,先处理业务再提交,避免处理失败却已跳位。三端都守住,消息就不丢。
Q2:怎么解决重复消费?先说明:只要选"至少一次"语义,重复就是不可避免的,所以重点是幂等 。生产者侧开启
enable.idempotence=true自动去重;更关键的是消费者侧幂等,用业务唯一键(比如 orderId)做去重,处理前查一下、处理过就跳过。框架层面也可以用数据库唯一索引兜底。
Q3:Kafka 消息顺序怎么保证?Kafka 只保证分区内有序。要保顺序,就把必须有序的消息用相同的 key 路由进同一个分区;同时注意
max.in.flight.requests.per.connection要配合幂等或设成 1,否则重试会乱序。全局有序要用单分区换,代价是吞吐,实际业务里基本不会用。
Q4:消息堆积了怎么处理?堆积的本质是消费速度跟不上生产速度。先看 lag 定位瓶颈在哪个分区、哪个消费者;然后优先优化消费逻辑(异步化、批量、缓存),别一上来就加机器;最后才是扩容,而且扩容消费者要先保证分区数够------同一消费组内一个分区同一时刻只能被一个消费者消费。
一句话总结Kafka 的三板斧就是"不丢靠三端配置,不重靠幂等,不乱靠分区内有序";实战遇到堆积,先查消费者再动集群。
结语
回到开头那家快递公司。配置得当的 Kafka,就像一家把丢件、重派、乱序全都防死了的顶级快递:寄件时要求回执、仓库里数据多存几份、派件先确认再签字、同城包裹强制按顺序发。而你------作为这条链路的负责人,只需要记住那一句贯穿全文的话:
Note
Kafka 的设计哲学是"尽力保证不丢、分区内有序",剩下的"重复"和"堆积",要靠你自己在消费者侧兜底。
现在,你可以用下面这份清单,快速检查自己的项目是不是每一环都到位了:
-
• 生产端
acks=all+retries已配置? -
• Broker 端
min.insync.replicas>=2? -
• 消费者手动提交 offset,先处理后提交?
-
• 消费者处理逻辑做了幂等(唯一键去重)?
-
• 需要保序的消息都用了相同的 key?
-
• 线上出现过堆积,是否已定位到消费者瓶颈?
这一路跟着包裹走下来,你手上的地图和话术应该都齐了。下次再聊,我们可以往"生产者事务"和"消费者重平衡"这两个更深的坑里钻------那里面还藏着不少让 Kafka 从"会用"到"精通"的细节。
本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
Kafka从入门到实战与高频面试题精讲:一条消息的不丢、不重、不乱之旅