12. 消息队列-Kafka进阶

文章目录


前言

每个 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,而在消费者。

排查三板斧,由便宜到贵依次上:

    1. 先定位瓶颈 :用 kafka-consumer-groups.sh 看每个分区的 lag,确认是哪个分区、哪个消费者卡住了
    1. 再优化消费逻辑:最常见的是消费者里同步查数据库、同步调远程接口------改成异步化、批量处理、加本地缓存,往往立竿见影
    1. 最后才扩容 :增加消费者数量的前提是分区数要够(同一消费组内一个分区同一时刻只能被一个消费者处理),所以扩容消费者通常要先扩分区,这是大家最容易踩的坑

# 查看消费组每个分区的 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从入门到实战与高频面试题精讲:一条消息的不丢、不重、不乱之旅


相关推荐
Thanks_ks20 小时前
Cookie 和 Session 到底是什么关系——登录态是怎么维持的
分布式·http·jwt·cookie·session·后端开发·登录态
千里达景区票务系统20 小时前
消息队列选型对比:RabbitMQ vs RocketMQ vs Kafka
kafka·消息队列·rabbitmq·rocketmq
车载中间件通信1 天前
[TC8测试]TCP_FLAGS_PROCESSING_06
数据库·网络协议·tcp/ip·中间件·tc8
Sayai1 天前
Kafka 跨数据中心日志同步实战:MirrorMaker 2 选型对比 + Flink 中继任务落地(双中心互备与消费者组隔离避坑)
大数据·运维·分布式·flink·kafka·消息队列·高可用
jianjinwssy1 天前
从零设计一个 Agent 安全沙箱:Docker 隔离、三级风控与 Kafka 审计
人工智能·docker·kafka
CodeGL1 天前
开源的分布式任务调度平台:XXL-Job
分布式·开源
一条泥憨鱼2 天前
【Kafka】零基础学习教程
java·分布式·学习·kafka
9624562 天前
补打一条卡,为什么要从前一天重新算:人工补录与实时重算业务复盘
java·mysql·kafka
kingx32 天前
网络安全事件应急响应:X集团 WebServer 入侵溯源与处置
中间件·web·应急响应·取证·恶意样本