第一次接触 Kafka 是在一个数据量突然涨起来的项目里,当时业务方要求所有订单操作都要实时同步到下游系统,数据库直连扛不住高峰流量,应用之间互相调用又经常把对方拖垮。折腾了一圈消息队列之后,我彻底倒向了 Kafka,后来一用就是好几年。这篇内容适合刚准备上手 Kafka 的后端开发、数据工程师,以及正在选型消息中间件但还没完全搞懂 Kafka 核心机制的读者。我会把概念、原理、安装、写代码、排坑这几件事一次讲透,不搞那种"复制一堆官方文档就算入门"的糊弄写法。
1. 先搞清楚 Kafka 到底解决什么问题
1.1 消息中间件的三个典型场景
很多人上来就学 Kafka 的 API,学完了也不知道它到底该用在哪儿。其实消息中间件解决的问题非常固定,就三类。
第一是解耦。A 系统产生订单数据,下游要对接库存、积分、短信、推荐系统、数据仓库,如果直接让 A 逐个调用这些接口,任何一个下游抖动都会拖慢主链路。引入 Kafka 之后,A 只需要把消息写进 Topic,谁需要谁自己去消费,A 不再关心下游有多少个系统、它们什么时候处理完。
第二是削峰填谷。秒杀、抢购、开票这类场景,流量峰值可能是平峰的几十倍。数据库直接面对这种冲击基本必死,正确做法是请求先落 Kafka,后端消费者按自己的节奏慢慢处理,把尖峰拉平。库存扣减、对账、通知这类不要求毫秒级响应的操作,都很适合走这个模式。
第三是异步化。用户注册成功后要发邮件、发短信、初始化各种缓存,这些操作加起来可能要 2 秒,但用户根本不想等。把事件写进 Kafka,主流程立刻返回"注册成功",通知类操作由消费者在后台处理,体感上快一大截。
我自己判断一个场景是否适合引入 Kafka,就一句话:这个数据是不是"事实"?订单创建、支付成功、用户登录,这些都是已经发生的事实,天然适合作为不可变的消息流。而那些需要来回交互、一问一答的 RPC 调用,就不该硬塞给消息队列。
1.2 Kafka 和其他消息队列的本质区别
入门阶段最常见的问题就是:RabbitMQ 和 Kafka 到底选谁?我的理解是这样的:RabbitMQ 的定位是灵活的路由和复杂的消费策略,它把 Exchange、Queue、Binding 这些概念做得非常精细,适合业务系统内部、消息量不大但规则复杂的场景。Kafka 的定位是分布式日志流平台,它本质上不是在"传递消息",而是在"存储和回放数据"。
这个区别决定了它们的设计完全不同。RabbitMQ 消费完一条消息基本就可以把它删掉,Kafka 却会把消息按照保留策略存一段时间,消费者可以反复读取、从任意位置重新消费。Kafka 的吞吐量可以做到单集群每秒处理百万条级别的消息,这背后靠的是顺序写磁盘、批量发送、零拷贝这些手段,后面我会展开讲。
还有个容易忽略的点:Kafka 的"消费者"不是随随便便拉数据的,它有一套以Consumer Group(消费组)为核心的并行消费模型,这是 Kafka 能扛住大规模数据分发的基础。RabbitMQ 的竞争消费者模式虽然也能并行,但 Kafka 的扩展性和回放能力要强得多。
1.3 理解 Kafka 的"高性能密码"
为什么 Kafka 能做到这么高的吞吐?一句话总结:它把随机读写变成了顺序读写,并且尽可能在内存和操作系统层面做文章。
传统消息队列处理一条消息要随机写磁盘,磁头来回找位置,慢是必然的。Kafka 的消息是 append-only 的,新消息永远追加到 Partition 文件末尾,这种顺序写盘在机械硬盘上都能跑出很高的速度,换成 SSD 更是夸张。读的时候也是顺序读,消费者按 Offset 往后读,操作系统会把热点数据放在 Page Cache 里,绝大多数读取直接命中内存,不需要落盘。
还有一个容易被忽略的点:批量。Kafka 生产端不会一条消息立刻发出去,而是攒一批再发,网络开销摊到每条消息上就非常小。配合压缩算法(比如 lz4、zstd),同样一条网络连接能承载的消息数量还能再翻几倍。理解了这个底层逻辑,后面配置生产者参数的时候你就知道 broker 在等什么了。
2. 核心概念:Topic、Partition、Consumer Group 一次讲透
2.1 Topic 和 Partition:数据是怎么分片的
Kafka 里最基础的概念是Topic(主题),你可以把它理解成数据库里的一张表,同类消息都往这个主题里写。但 Topic 并不是一个单点的队列,它会被拆成多个Partition(分区),每个分区是一个独立的、有序的消息日志。
为什么要拆分区?因为单机的存储和吞吐有上限,拆成多个分区之后,不同分区可以落在集群的不同 Broker 上,读写压力被分摊到多台机器。一条消息写入时,生产端会计算它应该去哪个分区,规则通常是"指定了 key 就用 key 的哈希值取模,没指定 key 就轮询"。同一个 key 的消息一定会进同一个分区,这在业务上非常重要——比如同一个订单的所有状态变更如果都进同一个分区,消费者就能保证按顺序处理它们。
Partition 内部是有序的,但Topic 整体不保证有序。很多人刚开始会踩这个坑:往一个 3 分区的 Topic 里并发写消息,消费的时候发现顺序乱了。这不是 Kafka 的 bug,而是设计如此——你要全局有序就只能用单分区,但单分区意味着把吞吐上限锁死了。所以实务上根本不要追求全局有序,只需要保证"同一个业务主键的消息有序"就够了。
分区还有个重要属性叫副本(Replica),每个分区的数据会在多个 Broker 上各存一份,其中一个是 Leader 分区负责读写,其余是 Follower 分区负责同步。Leader 挂了,集群会从 Follower 里重新选一个出来顶上。这就是 Kafka 高可用的基础。
2.2 Consumer Group:一组消费者怎么协作
光有分区还不够,Kafka 真正巧妙的是消费模型。每个消费者必须属于某个Consumer Group(消费组),同一个消费组里的消费者是"竞争关系"——一条消息只能被组内的一个消费者处理。这样设计的好处是,你可以用增加消费者数量的方式来水平扩展处理能力。
但这里有个关键约束:一个 Partition 在同一时刻只能被一个消费组内的一个消费者消费。也就是说,如果你的 Topic 只有 3 个分区,消费组里最多只有 3 个消费者能同时干活,加到第 4 个它就闲置了。这是入门阶段最常被误解的地方。所以前面说"分区数怎么定"特别重要,它直接决定了你的消费吞吐上限。
当消费者数量与分区数不匹配、或者消费者宕机、新增消费者时,消费组会自动触发一次Rebalance(再平衡),把分区重新分配给存活下来的消费者。Rebalance 期间整个消费组会短暂停止消费,如果频繁触发,你会看到消费延迟突然跳一下。这个问题后面排坑部分我会详细讲。
2.3 Offset:消费者进度是怎么记录的
Kafka 里每条消息在它的分区里都有一个编号,叫做Offset,从 0 开始递增。消费者每处理完一条消息,就把自己的读取位置往后挪,这个位置信息对每个消费组来说是独立保存的,存在 Kafka 内部一个特殊的主题里。
理解 Offset 是理解 Kafka 所有"怪现象"的钥匙。消息被消费之后并不会被删除,只要在保留时间内,新加入的消费组可以从头开始消费历史数据,老消费组也可以把 Offset 重置到任意位置重新读一遍。这就是 Kafka 的"回放"能力,也是它和传统消息队列最大的不同。
消费进度有两种管理方式。一种是自动提交,消费者在 poll 的时候自动把偏移量提交上去,省事但容易丢消息——因为它是定期提交的,如果消费者在处理过程中挂了,重启后会从上次提交的位置继续,中间处理过但没来得及提交的消息就被重复消费或者漏掉了。另一种是手动提交,业务代码处理完一批消息后自己调用 commit 方法,可控性强,推荐在生产环境用。手动提交又分 commitSync 和 commitAsync,前者同步阻塞保证提交成功,后者异步不阻塞但可能丢失提交,正确姿势是两者结合:正常用异步,关闭前或者失败重试时用同步。
2.4 消息的确认机制:至少一次、最多一次、精确一次
讲到消息可靠性,绕不开三种语义。最多一次:消息可能丢,但不会重复,适合日志采集这类丢了也无所谓的场景,代价是性能最好。至少一次:消息不会丢,但可能重复,这是 Kafka 默认能保证的语义,也是大多数业务场景的选择。精确一次:不丢也不重,需要在生产者、broker、消费者三端配合,开启幂等生产者、事务等机制才能做到,代价是性能损耗。
我必须说清楚:就算 Broker 层面帮你做到不丢,消费者层面依然可能重复消费。因为消费者处理完消息之后,还没来得及提交 Offset 就宕机了,重启后这条消息必然会被再读一遍。所以业务系统必须自己做幂等——用订单号作为唯一键去重,处理前先查一下是不是已经处理过了,或者利用数据库的唯一索引让重复写入直接报错。指望 Kafka 帮你做到精确一次,前提是你自己也把"处理"做成幂等的。
3. 本地搭建 Kafka:从下载到收发第一条消息
3.1 版本选择和环境准备
Kafka 的版本选择,我的建议是直接上当前主流的 3.x 系列。3.x 引入了KRaft 模式,可以把集群的元数据管理从外部协调组件中解放出来,部署复杂度大幅下降。早期的 Kafka 依赖那个外部协调组件来管 Broker 注册、Topic 元数据、Controller 选举,一套集群要维护两个系统,对新手极不友好。3.x 里你可以直接用 KRaft 模式跑起来,一个进程搞定。
先确认环境:JDK 8 以上(3.x 推荐 JDK 11 或 17),Linux 或者 macOS 都行,Windows 也能跑但不建议在生产用。下载安装包之后,解压到某个目录,比如/opt/kafka,然后设置一下环境变量KAFKA_HOME。注意 Kafka 安装包里自带了运行所需的脚本和默认配置,不需要额外安装别的东西。
动手之前先理解一下目录结构:bin/下面全是命令行工具脚本,操作 Topic、消费组、查看状态都靠它们;config/下面是各种配置文件,server.properties是 Broker 的主配置,producer.properties和consumer.properties是给命令行客户端用的。入门阶段,你 90% 的操作都在这几个目录里。
3.2 启动 Broker 并验证状态
KRaft 模式下,第一次启动需要先格式化存储目录。有个全局唯一的 ID 要先生成,然后写进配置文件,再执行格式化命令。
KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)" bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID -c config/server.properties格式化只需执行一次,之后启动 Broker 就是一条命令:
bin/kafka-server-start.sh config/server.properties看到日志里出现started (kafka.server.KafkaRaftServer)之类的字样,就说明 Broker 起来了。默认监听在localhost:9092,这是客户端连接的入口。
验证状态有个简单办法:随便开一个终端执行bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092,如果能正常列出各种 API 版本号,说明 Broker 的网络服务和认证都正常。这一步很多人跳过,结果后面客户端连不上,又排查半天。
3.3 用命令行工具完成首次收发
Kafka 的命令行工具是入门最好的老师,比 IDE 里写代码直观得多。先创建一个 Topic:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-topic --partitions 3 --replication-factor 1--replication-factor 1表示每个分区只存一份副本,单机环境只能这么配,生产环境至少 2 或者 3。创建完可以查看 Topic 详情,确认分区数和副本分布符合预期:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic test-topic然后开两个终端,一个运行生产者,一个运行消费者:
bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test-topicbin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic --from-beginning在生产者终端敲几行文字回车,消费者终端立刻就能看到。这里注意--from-beginning参数,它告诉消费者从 Topic 最早的 Offset 开始读,不加的话默认只消费启动之后产生的新消息。
建议你把"创建 Topic → 查看详情 → 命令行收发 → 查看消费组 Offset"这条链路多走几遍,顺手再敲一下bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group test-group看看消费组的 Lag 情况。命令行玩熟了,后面写代码出问题你就能快速用工具验证 Broker 本身是否正常,问题定位范围一下子缩小一大半。
4. 用代码连接 Kafka:生产者和消费者的正确打开方式
4.1 生产者 API 与核心参数
Kafka 的客户端库以 Java 为第一公民,官方文档和生态都以 Java 为主。用 Maven 引入依赖:
<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.6.0</version> </dependency>生产者的核心代码非常简单,难的是参数。看下面这段:
Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("acks", "all"); props.put("retries", 3); props.put("batch.size", 16384); props.put("linger.ms", 5); props.put("buffer.memory", 33554432); KafkaProducer<String, String> producer = new KafkaProducer<>(props); producer.send(new ProducerRecord<>("test-topic", "order-1001", "{\"status\":\"PAID\"}"), (metadata, exception) -> { if (exception == null) { System.out.println("发送成功,分区=" + metadata.partition() + ",offset=" + metadata.offset()); } else { System.err.println("发送失败:" + exception.getMessage()); } }); producer.close();ProducerRecord有三个关键参数:Topic 名、key、value。key 决定了消息进哪个分区,如果你需要同一个订单的消息有序,key 就传订单号。value 是消息体,生产环境一般传 JSON 或者序列化后的字节。send方法本身是异步的,回调会在发送完成或者失败时触发,这个设计是为了不阻塞业务线程。
close()方法很重要,它会等待所有未发送完成的消息刷出去再关闭。如果程序直接退出而不 close,缓冲区里的消息很可能直接丢光。
4.2 生产者的参数设计逻辑:acks、retries、batch.size、linger.ms
很多人把生产者参数当成填空题,照着抄一遍,出了问题不知道为什么。我挨个讲清楚背后的逻辑。
acks是可靠性最关键的参数,它决定生产者要求 broker 怎么确认消息。取值有三种:
| acks 取值 | 含义 | 可靠性 | 性能 |
|---|---|---|---|
| 0 | 发出去就算成功,不管 broker 收没收到 | 最差,可能丢消息 | 最快 |
| 1 | Leader 分区写入成功就返回确认 | 中,Leader 挂了可能丢 | 较快 |
| all | Leader 和所有 ISR 中的副本都写入成功才确认 | 最高,不丢消息 | 最慢 |
这个参数特别能反映你的业务诉求。日志采集、指标上报可以接受acks=0;订单、支付这类核心数据必须acks=all。千万别图省事默认值就行,默认其实是acks=1,对核心链路来说并不够安全。
retries是发送失败后的重试次数。注意重试可能导致消息乱序,因为第一条消息发送失败后,第二条可能已经发出去了,重试成功后第一条反而排到后面。要解决这个,可以把enable.idempotence设为 true(幂等生产者),它不仅能保证重试时不产生重复数据,还能帮你解决乱序问题,这是 3.x 版本非常推荐的做法。
batch.size和linger.ms是"攒批"的两个旋钮。batch.size是每个分区批次的内存上限(默认 16KB),linger.ms是等多久再发。二者配合决定了"攒多久、攒多少"。比如设置linger.ms=5,意味着最多等 5 毫秒,这期间来的消息都攒成一批发出去。把linger.ms从 0 调到 5~20,带宽利用率和吞吐量会有明显提升,代价是消息延迟增加几毫秒。实时性要求极高的场景(比如风控),可以保持linger.ms=0,让消息尽量快发。
最后一个容易忽略的是buffer.memory,它是生产者本地的发送缓冲总大小,默认 32MB。如果发送速度超过 broker 处理速度,缓冲会满,满了之后send方法会阻塞,超过max.block.ms就抛异常。看到TimeoutException或者BufferExhaustedException,很多时候不是网络问题,是这里触顶了。
4.3 消费者 API 与消费组实战
消费者这边,核心是一个 poll 循环。看代码:
Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("group.id", "order-consume-group"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("enable.auto.commit", "false"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("test-topic")); try { while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000)); for (ConsumerRecord<String, String> record : records) { // 这里写你的业务处理逻辑 System.out.printf("topic=%s, partition=%d, offset=%d, key=%s, value=%s%n", record.topic(), record.partition(), record.offset(), record.key(), record.value()); } // 处理完一批,手动提交 Offset consumer.commitSync(); } } finally { consumer.close(); }这段代码里最核心的是poll循环。Kafka 消费者不是"来一条处理一条",而是持续拉取一批消息到本地缓冲,然后业务循环慢慢处理。poll方法有几个隐藏行为必须知道。
第一,poll不光是拉数据,它还承担了心跳功能。消费者必须每隔一段时间调用一次poll,让 broker 知道它还活着。如果两次poll间隔超过了max.poll.interval.ms(默认 5 分钟),broker 会认为这个消费者挂了,触发 Rebalance,把它的分区分给别的消费者。这就是著名的" consumer 处理太慢被踢出消费组"问题——如果你的业务处理一条消息要 10 分钟,不做任何配置调整,一定会被踢。
解决思路有两个方向:调大max.poll.interval.ms,或者减小max.poll.records(每次 poll 返回的最大条数),让单次处理时间缩短。我见过很多团队为了省事只调大超时时间,实际上更健康的做法是控制单批次大小,并给处理逻辑加上合理的超时和重试。
第二,group.id是消费组的身份标识。同一个消费组里的多个消费者实例会自动分配分区,实现并行消费;不同消费组订阅同一个 Topic,可以各自独立消费全部数据,互不影响。这个特性让 Kafka 天然支持"一份数据被多个业务方各取所需"。
第三,手动提交commitSync的位置要在业务处理成功之后,而不是之前。顺序反了,case 就是"消息处理失败但 Offset 已经提交,重启后这条消息永远丢了"。
5. 生产环境落地前必须想清楚的几件事
5.1 分区数怎么定:别拍脑袋
分区数是最难改的配置之一。Topic 创建之后可以手动增加分区,但不能减少,一旦设多了就只能一直带着。而且分区数关系到消费并行度、文件句柄数量、Leader 选举时间,怎么定需要认真想。
我见过不少团队拍脑袋定了 12 个分区,理由是"看起来吉利",结果数据量很小,白白浪费资源。分区数应该由两个因素决定:目标吞吐量和单分区吞吐能力。先压测出单分区每秒最多处理多少条消息(写入和消费都要测),再用目标吞吐量除以单分区能力,得到一个基础分区数,最后在这个基础上适当留一点余量,应对流量增长。
还要考虑业务维度。如果你的数据需要按某个 key 保证顺序,分区数是不能被这个 key 的分布随意拉扯的——key 是哈希路由到分区的,无论分区多少,同一个 key 永远落在同一个分区,所以顺序性不受影响。影响的是均衡性:key 的取值如果很少且分布极度不均,某些分区会特别忙,此时再多分区也白搭。
另外记住一个公式:消费组能同时消费的分区数 = min(消费者数量, 分区数)。分区配了 12 个,如果消费端只有 3 个实例,那实际并行度就是 3。加实例可以提升并行度,但如果实例数已经超过分区数,新增实例就是纯浪费。所以最佳实践是消费组实例数约等于分区数,宁可稍微少一点,也别多出一堆闲置实例。
5.2 副本因子与 ISR:数据安全的第一道防线
副本因子(replication factor)决定了每个分区的数据在集群里存几份。单机 Demo 用 1,生产环境至少 3。副本不是摆设,Leader 分区不可用时,ISR(In-Sync Replicas,同步副本)集合里的 Follower 会被提升为新的 Leader,继续对外服务。
这里有个核心概念叫 ISR。它指的是"跟得上 Leader 数据"的副本列表。如果某个 Follower 同步太慢,或者网络分区导致它落后太多,它会被踢出 ISR。acks=all的含义是"ISR 里所有副本都写入成功才算成功",所以 ISR 集合越大,消息越安全。但如果 ISR 里只剩 Leader 自己,acks=all退化成acks=1的效果,那就危险了。
对应到配置上,有两个参数必须一起调整。min.insync.replicas表示至少要有几个副本在 ISR 里才允许写入,生产环境我建议设为 2。设了它之后,如果某个分区只剩 Leader 一个存活副本,写入会直接报NotEnoughReplicasException,而不是默默接受数据丢失风险。这个"宁可写不进去,也不能丢数据"的思路,对核心业务链路特别重要。
还有个容易忽略的点:副本因子和分区数一样,创建 Topic 时就要规划好。虽然可以后期手动增加副本,但操作复杂且涉及数据搬迁,最好一次性定对。副本也不是越多越好,每个副本都是一份全量数据,3 个副本意味着 3 倍的存储开销,磁盘成本要提前算好。
5.3 监控和运维的常用手段
入门阶段最容易犯的错误是:把消息发出去就不再管了,直到业务方反馈"数据没到"才开始查。Kafka 运维的核心就一个字——Lag。Lag 是消费组当前 Offset 和生产端最新 Offset 的差值,它代表"有多少消息积压没被处理"。Lag 持续增长说明消费能力跟不上生产速度,这是最需要关注的健康指标。
查看消费组 Lag 的命令:
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group order-consume-group输出里会列出每个分区的当前 Offset、Log End Offset 和 Lag。建议把它写进定时任务,配合监控告警,Lag 超过阈值就报警。
Kafka 还提供了一批管理类命令,比如查看所有 Topic 列表、查看 Broker 状态、修改 Topic 配置等。生产环境我强烈建议用专门的监控面板或者可视化工具统一查看集群状态,这些工具可以在界面上直接看到分区分布、Broker 存活、吞吐量、Lag 变化趋势,比一条条敲命令直观得多。另外,磁盘使用率一定要盯紧,Kafka 的日志是会持续增长的,即使设置了保留时间,数据量大的 Topic 也可能撑爆磁盘。我曾见过磁盘满导致整个集群写入阻塞的事故,代价非常大。
6. 入门阶段最容易踩的坑:我的排查实录
6.1 坑一:消费组积压严重但 CPU 没跑满
第一次遇到这个问题是在一个订单事件处理的场景里。业务反馈消息延迟越来越大,我看 Lag 已经到几万条了,但消费者的 CPU 占用率只有 20%,磁盘和网络都正常。直觉告诉我不是机器性能问题,而是并行度或者配置出了问题。
排查链路是这样的:先看消费组详情,发现 Topic 只有 2 个分区,消费组里却配了 6 个消费者实例。这就解释了 CPU 为什么不满——Kafka 的规则是一个分区同时只能被组内一个消费者消费,6 个实例里有 4 个完全闲置,实际干活的只有 2 个。解决方案也很直接:要么把分区数调大(对存量 Topic 只能增加分区),要么把消费者实例数降到和分区数一致。减少无用实例之后,浪费的资源被释放,问题立刻缓解。
这个案例给我的教训是:看到积压先数分区,再数消费者,两者的匹配关系是第一个要确认的事。
6.2 坑二:生产者发送超时
另一次是在压测阶段,生产者频繁报TimeoutException。当时第一反应是网络问题,结果抓包发现网络延迟很正常。排查到最后,问题出在buffer.memory上。
压测时我把linger.ms调到了 20,消息量一大,生产者本地的发送缓冲迅速被占满,send方法阻塞等待空间释放,超过max.block.ms就抛异常。网络和 broker 都没问题,纯粹是生产端缓冲配置跟不上写入节奏。
解决办法:调大buffer.memory,适当降低linger.ms,同时给max.block.ms留足余量。这里我想多说一句,生产者的异常信息很多时候指向broker 不可达,但实际原因可能在生产端自己,排查一定要把生产端缓冲、网络、broker 三个层面都过一遍,别一上来就盯网络。
6.3 坑三:消息重复消费
还有一次是下游数据库出现了大量重复订单记录。我们当时用的自动提交 Offset,消费者处理完消息后还没来得及提交,进程就被重启了,重启后从上次提交的 Offset 继续读,已经处理过的消息自然被再处理一遍。
这个问题的根源不是 Kafka 丢消息,而是消费者处理完和提交 Offset 之间有空窗期。修复分两步:代码层面改成手动提交,并且保证"先处理业务,再提交 Offset";业务层面给订单事件加上幂等控制,用订单号建立唯一索引,重复插入直接报错拦截。两步都做了之后,重复问题彻底消失。
后来我形成了一条习惯:凡是进 Kafka 的核心业务消息,value 里必须带上业务唯一标识,消费端第一件事就是查重。这是所有消息系统的必修课,跟用哪个中间件无关。
6.4 一个额外的提醒:版本兼容性
最后提醒一个很隐蔽的坑:客户端版本和 Broker 版本不一致。Kafka 的客户端和 Broker 之间有协议兼容机制,老版本客户端连接新版本 Broker 通常没问题,但如果你用了新版本客户端的特性,而 Broker 版本太老,会碰到各种奇怪的行为,比如新分区分配策略不生效、事务功能报错。
我的习惯是尽量保持客户端版本和 Broker 大版本一致,至少保证小版本不落后太多。升级的时候先升级 Broker,再逐批升级客户端,切忌只升级一头。
最后聊几句
内容写到这里,Kafka 入门该知道的东西基本都覆盖了。如果让我给一个学习路径建议,那就是:先把命令行工具玩明白,再写代码;先把概念想清楚,再调参数。Kafka 的所有参数背后都有明确的设计逻辑,理解了为什么,配置就不再是抄作业。
我个人还有一个习惯,每接触一个新的 Kafka 环境,第一件事就是用命令行工具创建 Topic、收发消息、查看消费组 Lag,确认端到端链路是通的,然后再开始写业务代码。这个习惯帮我避开了无数"代码报错其实是环境没配好"的伪问题。
入门阶段不需要追求把每个参数都吃透,先把acks、分区数、消费组、Offset 这几个核心概念彻底搞懂,剩下的都是在实践中逐步积累的。等你真正经历过一次积压、一次重复消费、一次扩容之后,再看 Kafka 会完全是另一种感觉。