Kafka 从零开始

用订单事件学懂日志、分区与独立消费进度

Aias00

2026-10-21

写在前面:你会学到什么

如果你刚听说 Kafka,却不知道“消息队列”“分区”“消费者”是什么意思,这本书就是为你准备的。你只需要会打开终端、复制命令,不需要先学分布式系统,也不必先写 Java。

读完并完成练习,你应该能够:说清 Kafka 解决什么问题;在自己的电脑上启动一个练习环境;发送和读取订单事件;观察同一份数据被不同业务独立读取;解释消息为什么可能重复、为什么重启后没有重新读取;知道生产环境还缺哪些条件。

本书将 Kafka 作为“可保留、可被多个业务独立读取的分区日志”来学习。若你已经读过 RocketMQ,这里的重点增量是日志位置与消费进度彼此独立:通知组可以暂停,统计组继续读取;通知组恢复或回放时,仍使用自己的书签。两种系统都支持消息与消费协作,选型应结合具体能力,不能仅凭“队列”和“日志”两个称呼下结论。

全书围绕三个问题推进:记录还在不在,由保留与压缩策略决定;某个业务读到哪里,由组偏移量决定;当前谁负责哪个分区,由组分配与再均衡决定。把这三条时间线分开,才能解释回放、扩缩容与故障重启。示例只观察真实 Kafka 单节点和命令行消费者,不冒充带数据库、短信或支付的完整应用。

本书是一条循序渐进的路线,不是参数百科。建议每天学习一到两章。第一次阅读先完成第 1 至 8 章,再学可靠性和项目。遇到名词可以翻到词汇表。

本书依据 Kafka 4.1 系列官方文档,示例固定到官方快速入门当前列出的 4.1.2,使用 KRaft,不需要 ZooKeeper。这是为了让命令有明确版本,不代表断言它是所有时间点的最新版本。官方网页会更新,资料访问日期为 2026 年 10 月 20 日。附录给出可直接打开的来源。

为了照顾初学者,正文把“消息”“事件”“记录”作为接近的说法。严格地说,事件强调“发生过的事实”,记录是 Kafka 实际保存的数据单位。代码块前面的解释会说明在哪里执行;复制命令时,不需要额外输入 $ 或 > 提示符。

第 1 章 一笔订单为什么需要 Kafka

先从一个熟悉的场景开始

你在购物网站按下“下单”。订单服务把订单写进数据库,随后还需要通知库存、发送短信、更新销售统计。最直接的做法是订单服务挨个调用这些系统。

这会产生两个问题。第一,慢系统拖累下单:短信服务卡了五秒,用户也可能等待五秒。第二,业务彼此绑得很紧:每增加一个新系统,订单服务都需要增加调用逻辑。

我们可以改成:订单服务记录“订单已经创建”这个事实,把它交给 Kafka;短信服务、统计服务分别从 Kafka 读取它。订单服务不用知道所有读者是谁。

订单服务 ──发送──> Kafka 的 orders 主题
                       ├──> 通知服务
                       ├──> 销售统计
                       └──> 风控分析

这叫作异步解耦。“异步”表示后续工作不必全部在同一次请求中结束;“解耦”表示系统减少对彼此实现细节和可用性的依赖。

再理解削峰和回放

假设平时每秒 100 个订单,促销时变成每秒 5,000 个。下游每秒只能处理 1,000 个,Kafka 可以暂时保存来不及处理的记录,让下游稍后继续。这叫削峰填谷。它不会凭空增加处理能力:流量长期高于消费能力时,积压仍然持续增长,最终会遇到存储或保留时间边界。

如果后来新增一个“订单地区分析”系统,它可以读取仍被保留的历史记录。这叫回放。已经被保留策略删除的数据不能靠修改消费进度找回来。

Kafka 到底是什么

Kafka 是分布式事件流平台。现在先把它想象成一套“可多人订阅、按序追加、保存一段时间的日志”。日志在这里是一串数据记录,不只是程序报错文件。

Kafka 适合持续产生的大量事件、多个独立读者、需要历史回放的业务。它不自动替你完成库存事务、短信重试和订单查询。只需要少量后台任务时,更简单的任务队列也可能足够;按订单号查当前状态,通常仍然使用业务数据库。

本章自测:Kafka 能让慢消费者永远不扩容吗?不能。它能缓冲压力,不能消除长期的处理能力缺口。

第 2 章 用核心概念搭好地图

生产者、主题和消费者

生产者(Producer)负责写入,例如订单服务。主题(Topic)是一类记录的逻辑集合,例如 orders。消费者(Consumer)负责读取,例如通知服务。Kafka 服务进程称为 Broker,一个集群通常有多个 Broker。

生产者把记录交给主题,而不是直接指定“发给某个消费者”。消费者可以暂时离线,之后再读取仍然保留的数据。

分区和偏移量

主题可以拆成多个分区(Partition)。每个分区是一条独立的追加日志。记录在某个分区里的位置叫偏移量(Offset)。

orders
  分区 0:offset 0 → offset 1 → offset 2
  分区 1:offset 0 → offset 1
  分区 2:offset 0 → offset 1 → offset 2 → offset 3

“分区 0 的 offset 2”和“分区 2 的 offset 2”是不同记录。偏移量只在同一分区内有意义,也不应被当作精确的业务消息计数:压缩、事务和日志管理都可能让可见记录的位置有空洞。

Kafka 保证的是分区内的日志顺序,不是整个多分区主题的全局顺序。即使使用同一分区,消费者把任务交给多个线程后,也可能自行打乱完成顺序。

Key 和消费者组

一条记录通常包含 Key、Value、时间戳,也可以带 Headers。Key 可以是订单号,Value 可以是 JSON。固定分区数量、固定分区策略的前提下,同一个 Key 通常进入同一个分区,因此适合把同一订单的状态变化放在一起。增加分区或改变分区策略后,历史与新记录的映射可能变化。

消费者组(Consumer Group)是一组协作的消费者,使用相同的 group.id。同一组里,一个分区在稳定分配时交给一个消费者负责;不同组各自维护进度,都能独立读取同一份记录。

记住一句话:组内分工,组间独立。 三个分区、四个组内消费者,通常最多三个消费者同时获得分区;多出来的一个没有分区可读。

副本

副本(Replica)是分区数据的复制。普通分区有一个 Leader 负责写入,Follower 从 Leader 复制。Leader 故障后,满足条件的副本可以接替。副本数和分区数不是同一个概念:三个分区、每分区三个副本,总计九份分区副本。

本章自测:两个业务都要完整读到订单,该用同一个组吗?不该。使用不同组;同一个组意味着协作分担。

第 3 章 准备一个安全的练习环境

选择本地脚本路线

本书统一采用官方压缩包与本地脚本,减少“主机地址”和“容器地址”混用。命令适用于 macOS、Linux,Windows 建议在 WSL2 的 Linux 终端运行。WSL2 里的 Java 和文件目录也应在对应 Linux 环境内准备。

你需要 Java 17 或更高版本、约 2 GB 可用内存和至少数 GB 的空闲磁盘。建议使用 Java 21。先执行:

java -version

预期输出含 17、21 或更新的受支持 Java 版本。如果系统提示找不到运行时,先安装 JDK 并确认 JAVA_HOME、PATH 指向它。看到一个 java 命令路径不代表真的装好了运行时。

到 Apache 官方下载页 或 4.1.2 归档目录 获取 kafka_2.13-4.1.2.tgz。选择二进制包,不是 source 源码包。下载目录提供签名和 SHA-512 文件,可按官方说明校验。

在下载文件所在目录执行:

tar -xzf kafka_2.13-4.1.2.tgz
cd kafka_2.13-4.1.2

文件名里的 2.13 是构建使用的 Scala 版本标记,不要求你先学 Scala。

隔离本书的数据

Kafka 的日志数据和程序日志不是一回事。我们把数据放在解压目录下,避免误碰其他练习实例。以下操作只用于新解压的练习目录,不要拿去格式化已有集群。

mkdir -p book-data
cp config/server.properties config/book.properties
python3 - <<'PY'
from pathlib import Path
p = Path('config/book.properties')
s = p.read_text()
data = str(Path('book-data').resolve())
lines = [line for line in s.splitlines()
         if not line.strip().startswith(('log.dirs=', 'metadata.log.dir='))]
lines.append('log.dirs=' + data)
p.write_text('\n'.join(lines) + '\n')
print('练习数据目录:' + data)
PY

这一步额外需要 Python 3。若没有 Python,也可以用文本编辑器把 config/book.properties 中的 log.dirs 改成练习目录的绝对路径,并确保没有另一条 metadata.log.dir 指向旧目录。练习路径尽量不含空格。

初始化并启动

只在第一次初始化时执行这两条:

KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
bin/kafka-storage.sh format --standalone \
  -t "$KAFKA_CLUSTER_ID" -c config/book.properties

随后启动:

bin/kafka-server-start.sh config/book.properties

这个终端会持续显示日志。保持它开启。另开终端时,都先 cd 到同一解压目录,再执行后文命令。localhost:9092 是本机 Broker 的访问地址。

重启时只运行启动命令,不要重新生成集群 ID 或再次格式化。停止时在服务终端按 Ctrl+C,等待进程退出。本地单节点同时承担 Broker 与 KRaft Controller 角色,用于学习;生产集群需要专门规划角色、仲裁和故障域。

如果你已经熟悉 Docker,官方还有 apache/kafka:4.1.2 镜像。本文后续命令使用解压目录中的 bin/,不要直接把容器内路径与本地路径混用。

第 4 章 发出和读到第一条消息

创建单分区主题

在新的终端,进入 Kafka 解压目录,执行:

bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic hello-kafka \
  --partitions 1 --replication-factor 1

预期看到 Created topic hello-kafka.。若提示已经存在,可以继续使用;本章的消息数预期以第一次创建的空主题为准。

查看它:

bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic hello-kafka

重点找 PartitionCount: 1 和 ReplicationFactor: 1。Leader、Replicas、Isr 后面的数字是节点编号,不是消息数量。

写入三行

bin/kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic hello-kafka

进入交互界面后,输入下面三行,每行后回车:

你好,Kafka
这是第二条消息
订单 A1001 已创建

每一行是一条独立记录。命令不会自动把多行 JSON 合成一条消息。按 Ctrl+C 退出生产者,Broker 仍然保持运行。

从头读取

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic hello-kafka --from-beginning \
  --group hello-readers

第一次使用这个组,应读到刚才三行,然后持续等待新消息。等待不是卡死。打开另一个生产者再输入一行,你会看到消费者很快显示它。

消费不会把记录从主题里删掉。停止消费者,再换一个从未使用过的组,比如 hello-readers-2,仍可以从头读取保留着的消息。

为什么重启后没有从头显示

--from-beginning 并不是“强行覆盖已有消费进度”。在没有有效已提交进度时,它让消费者从最早的可用位置开始。已有组通常从保存的进度继续。需要从头演示时,最简单的方法是换一个新组;正式重放需要有计划地重置进度。

本章检查点:你应当能看到消息;能解释主题不会因消费而清空;能区分“新组从头读”和“旧组继续读”。

第 5 章 用订单号理解 Key 和分区

建立三个分区的主题

bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic orders \
  --partitions 3 --replication-factor 1

接着启动带 Key 的生产者:

bin/kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic orders \
  --property parse.key=true \
  --property key.separator='|'

输入以下三行,注意每行中间的 |:

A1001|{"event_id":"e-001","order_id":"A1001","type":"OrderCreated","amount_fen":9900,"schema_version":1}
A1002|{"event_id":"e-002","order_id":"A1002","type":"OrderCreated","amount_fen":2000,"schema_version":1}
A1001|{"event_id":"e-003","order_id":"A1001","type":"OrderPaid","amount_fen":9900,"schema_version":1}

左边是 Key,右边是 Value。金额用“分”的整数表示,避免用二进制浮点数直接表示钱。event_id 标识一次业务事件,order_id 标识一笔订单,两者用途不同。

显示记录位置

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group orders-inspect-v1 \
  --property print.key=true \
  --property print.partition=true \
  --property print.offset=true

你应看到分区、偏移量、Key 和 JSON。观察 A1001 的两个事件:在本书默认分区策略和未改分区数时,它们进入同一分区,偏移量按追加顺序增大。不同订单也可能落入同一分区,Key 与分区不是一对一关系。

不能依赖屏幕显示的三行恰好是发送时的全局顺序,因为消费者从多个分区取数据。也不要背下“A1001 一定进分区几”,分区映射不是业务规则。

业务顺序还有额外条件

把同一订单作为 Key 只是基础。若两个生产者同时写同一订单,“谁先发生”的业务顺序仍需要由上游协调,或者在事件里带业务版本号。消费者若并行处理同一分区,也要保证同一订单的处理顺序。

新增分区可以增加并行度,但可能改变 Key 映射。需要严格保持历史与后续顺序时,应先设计迁移和切换方案,而不是在高峰时随手扩分区。

练习:把 A1001 改成固定 Key all-orders 会怎样?所有这些消息会集中到同一个分区,失去按订单分摊流量的优势。

第 6 章 消费者组:独立进度、协作与再均衡

先做“组间独立”实验

开两个消费者终端,使用不同组:

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group notification-demo
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group analytics-demo

首次运行且历史数据仍保留时,两边都能读到全部三条记录。再发送一条新订单,两边都会看到。这分别对应通知服务与统计服务。

再做“组内分工”实验

开两个终端,执行同一条命令:

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group workers-demo \
  --property print.partition=true --property print.key=true

这两个消费者共同分担三个分区。历史记录可能在第二个进程启动前已经被第一个读完,因此不要只看启动时的输出。等组稳定后,再通过上一章的生产者发送多个不同 Key 的新事件。

查看分配和进度:

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group workers-demo

同一个分区会对应一个当前负责它的消费者。同一个消费者可以负责多个分区。不同 Key 可能哈希到同一个分区,所以少量事件不保证两个终端都立刻有输出。

什么是再均衡

组里有人加入、退出,或者订阅主题的分区发生变化时,分区分配需要调整,这叫 Rebalance,中文常译“再均衡”。退出一个消费者后,剩余成员会接管它的分区。

再均衡可能造成短暂等待,也可能让尚未提交进度的记录被再次处理。业务应用需要正确处理分区撤销、任务结束和提交进度。Kafka 4.x 有新的消费者组协议能力,但具体启用方式受客户端和 Broker 配置影响;本书不依赖把旧客户端参数原样迁移到新协议。

用分配表验证接管

判断再均衡是否完成,不能只看“第二个进程启动成功”。先确认三个分区被一个成员覆盖,加入第二个后确认仍恰好覆盖这三个分区且由两个成员分担,再停止第二个,等待第一个重新负责全部分区。观察期间可能出现临时无分配或过渡状态,应等待稳定后再比较。

随书 scripts/run_lab.py 按这三个阶段读取组描述并检查成员与分区覆盖。它证明受控启动、加入与退出后的分配行为,不证明真实业务的在途任务已经正确交接;异步业务仍要自行处理撤销、提交与幂等。

本章自测:把消费者数量从 3 加到 30,就能让三个分区快十倍吗?不能。先看分区并行度,再看真正的瓶颈。

第 7 章 消费进度:书签记录在哪里

读到了与处理完了是两件事

消费者有“当前位置”,表示接下来取哪里;还有“已提交偏移量”,表示组恢复时从哪里继续。通常提交的是下一条要处理的记录位置。如果分区 0 的 offset 0、1、2 已经成功完成,应提交 3。

已完成:0  1  2 | 下一条:3  4  5
提交进度:3

消费者组的进度由 Kafka 管理,通常保存在内部主题 __consumer_offsets。它不是刻在每条业务记录上的“已读”标记,所以多个组能拥有不同书签。

为什么会重复

考虑以下时间线:消费者读到 offset 2;短信已成功发送;进程突然退出;offset 3 还没提交。重启后,它可能再次读到 offset 2,再发送一次短信。

如果反过来,先提交 offset 3,再发短信,进程在两步之间退出,短信就可能永远漏发。

这不是调一个超时时间就能彻底解决的问题,而是外部业务结果与 Kafka 进度之间缺少一个共同的原子提交边界。后文会讲幂等和事务。

Lag 是什么

上一章的组描述通常包含 CURRENT-OFFSET、LOG-END-OFFSET 和 LAG。可以先近似理解为“已经提交的位置”“日志末端位置”和“两者差距”。

Lag 反映偏移量距离,不等于精确的待办订单数量,也不等于等待秒数。事务、压缩和记录可见性会影响解释。更有用的是同时观察 Lag 趋势、事件时间到处理完成的延迟、处理吞吐和错误率。

练习:有意识地回放

先停止 hello-readers 组的所有消费者,再预览:

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group hello-readers --topic hello-kafka \
  --reset-offsets --to-earliest --dry-run

确认只涉及本书的练习组和主题后执行:

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group hello-readers --topic hello-kafka \
  --reset-offsets --to-earliest --execute

重启该组消费者,就会从最早保留位置继续。若对真实扣款业务回放,可能重新触发副作用,因此生产回放必须明确目标范围并先确保幂等。

重置成功只说明书签被修改,还应重新启动同一个组,实际核对三条中文记录确实再次读到;同时比较另一业务组的已提交位置,确认没有被一起重置。完整回放方案需要记录目标主题和分区、起止位置、操作者、业务幂等依据以及停止条件。需要独立重算又不想修改原组时,可以用新的组读取保留日志,但新增读者仍会消耗集群资源。

第 8 章 数据保存多久:消费和删除彼此独立

保留策略

Kafka 通常按时间或大小保留日志,清理由后台按日志段推进。retention.ms 表示时间策略,retention.bytes 是每个分区的大小策略;设置了两者时,任一条件都可能推动旧日志段清理。

例如,为 orders 设置七天时间保留:

bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics --entity-name orders --alter \
  --add-config retention.ms=604800000

这里 604800000 毫秒等于七天。它不是保证每条记录精确存在七天的计时器。段滚动、检查周期、时间戳等都会影响实际清理时点。消费者落后太久时,想读的位置可能已被删除。

日志压缩

cleanup.policy=compact 用于按 Key 保留最新状态的场景,例如用户资料变更流。压缩在后台运行,不会立即变成“每个 Key 恰好一条”。删除语义通常用相同 Key、Value 为 null 的墓碑记录表达;墓碑也有自己的保留和清理条件。

订单创建、支付、退款是一串有价值的历史事实,随意只保留订单最后一次事件,可能破坏审计和重算。需要完整过程时,先考虑按时间保留事件流;需要状态视图时,再设计压缩主题。

配置压缩不等于清理已经发生

可以为状态视图单独建立一个练习主题,不要直接改变保存完整订单历史的 orders:

bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic order-state --partitions 1 --replication-factor 1 \
  --config cleanup.policy=compact
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics --entity-name order-state --describe

读回 cleanup.policy=compact 证明配置已生效,不证明后台清理器已经去掉某条旧值。本轮实验仅核对保留和压缩配置,未通过加速段滚动观察物理删除或墓碑清理。压缩可能保留同一 Key 的多个版本一段时间,offset 也不会被重新连续编号;消费者必须能容忍位置空洞。对重要业务,应另行验证清理延迟、恢复窗口与状态重建。

粗算磁盘

假设平均每秒 1,000 条,每条序列化后约 1 KB,保存七天:原始写入约 1 MB/秒,一天约 86.4 GB,七天约 604.8 GB;三个副本约 1.81 TB。这里用十进制单位估算。

真实容量还要考虑压缩率、Key 与协议开销、索引、峰值、故障副本恢复和磁盘余量。别把这道估算题当成采购结论。监控磁盘增长速度,比只看配置更可靠。

本章自测:某组把所有消息读完,会释放主题磁盘吗?不会。删除主要由保留、压缩或显式管理操作决定。

第 9 章 可靠性:一条消息究竟算不算送达

先给成功一个边界

生产者返回成功、Broker 保存记录、所有副本完成复制、消费者读到、业务数据库更新成功,是不同阶段。讨论“不丢”或“不重复”时,必须说明在哪个阶段、容忍哪些故障。

生产者发出请求但没有收到回应,也不等于 Broker 没收到:请求可能成功了,只是响应在网络里丢了。这也是重试可能制造重复的原因。

acks、副本和 ISR

acks=0 不等待服务端确认,吞吐需求不能让它自动变可靠。acks=1 等待 Leader 确认,但此时尚未复制的数据可能在 Leader 故障时丢失。acks=all 等待当前同步副本集合 ISR 的确认。

ISR 是与 Leader 保持足够同步的副本集合。min.insync.replicas 决定允许接受这类可靠写入的最低 ISR 数量。常见设计起点是副本数 3、min.insync.replicas=2、生产者 acks=all:当 ISR 小于 2 时拒绝写入,以牺牲部分可用性保护可靠性。这仍要求正确的故障域、存储、选主等条件。

本书单节点只有一个副本,不能靠把 acks 调成 all 得到三台机器的保护。也不能在只有一个副本的主题上设 min.insync.replicas=2 后期待照常写入。

生产者幂等

Kafka 4.1 Java 生产者在没有冲突配置时默认启用幂等。显式 enable.idempotence=true 时,相关条件包括 acks=all、retries>0、max.in.flight.requests.per.connection<=5。不要照搬“必须把 in-flight 设成 1”的旧教程。

生产者幂等主要避免协议重试造成的日志重复。它不识别业务上两次分别调用 send 的同一订单事件,也不自动防止消费者重复发短信。

建议应用显式检查发送完成结果,对异步发送处理回调或 Future;只把记录放进客户端缓冲区就立刻退出程序,不能证明记录已成功送达。

三种处理语义

语义 典型失败结果 适用思考
至多一次 可能漏处理,通常不重试 能接受丢失的场景才考虑
至少一次 尽力避免漏处理,但可能重复 配合业务幂等很常见
恰好一次 在明确事务边界内只产生一次有效结果 必须讲清系统和故障假设

“恰好一次”不是万能开关。Kafka 读入、处理、写回 Kafka 的链路可以把输出与消费进度放进同一个 Kafka 事务;下游需要适当使用 read_committed。Kafka Streams 也提供相应的处理保证。普通数据库更新或外部支付接口不自动包含在这个事务里。

本章自测:设置生产者幂等后,短信还能重复吗?能。短信副作用与消费进度之间仍需业务设计。

第 10 章 把重复变成可承受:幂等与事务发件箱

本章提供数据库幂等与 Outbox 的设计推导,没有附带数据库、CDC 转发器或真实支付调用。第 11 章的 CLI 项目验证日志和消费进度,不是本章事务设计的端到端实现。Kafka 事务的原理说明也不代表本轮做过跨系统事务实验。

用事件 ID 识别同一次事件

幂等的意思是:同一个业务操作执行多次,最终有效结果与执行一次相同。例如对 event_id=e-001 的积分增加只能生效一次。

常见做法是在数据库中建立处理记录表,以 (consumer_name, event_id) 为唯一键。消费者在同一个数据库事务中插入去重记录并更新业务表。唯一键冲突说明这一业务消费者已处理过该事件,可以跳过业务更新。

读取事件
  ↓
开始数据库事务
  ├─ 插入处理凭证(唯一约束)
  └─ 执行业务更新
提交数据库事务
  ↓
提交 Kafka 消费进度

若数据库事务成功、Kafka 进度未提交,重启后会再次读到;数据库唯一约束让第二次更新失效。若业务事务失败,不能提交进度假装成功。

去重表不是随便查一下

“先查没有,再插入”会有并发竞争,必须由数据库唯一约束和事务保证正确性。去重记录保留期还要覆盖可能重放的历史,否则删除去重凭证后,老事件可能再次生效。

外部发短信或支付不能和普通本地数据库事务自然原子化。优先使用对方提供的幂等请求号,或把待执行任务写入本地发件箱,再由独立执行器按幂等规则调用并核对结果。

数据库写成功,消息没发出去怎么办

订单服务面临另一个问题:先写订单库,再发 Kafka,两步之间可能宕机;先发 Kafka,再写库,也可能产生没有真实订单的事件。

事务发件箱(Transactional Outbox)的思路是,在同一个数据库事务里写订单表与 outbox 表。后台转发器或 CDC 工具再把 outbox 的事件送进 Kafka。转发也可能重复,因此消费者仍应幂等。

这提供的是可恢复的协调方式,需要监控未转发事件、重试、顺序和清理,不等于“装一个插件就自动解决所有一致性问题”。

处理失败与坏消息

短暂网络失败可以有限重试并退避;字段格式错误反复重试通常不会自己变好。达到边界后,可以送入死信主题,记录原主题、分区、偏移量、事件 ID、错误原因和失败时间。

死信需要告警、责任人、修复和重放流程。若把同一订单的失败事件移走后继续处理后续事件,业务顺序可能改变。把它作为明确的业务决策,而不是默认“跳过就好”。

第 11 章 一份可以交付的小项目

项目目标与事件契约

现在你要完成一个“订单事件观察站”:订单生产者写入 orders;通知组和统计组独立读取;能观察分区和组进度;可以演示停止、恢复和回放。它不需要真的发送短信或扣款,终端输出代表下游收到事件。

事件的约定叫契约。项目采用以下字段:

字段 含义 示例
event_id 一次事件的唯一标识 e-101
order_id 业务订单号,同时作为 Key A2001
type 已发生的事实类型 OrderCreated
amount_fen 金额,整数分 5800
schema_version 数据格式版本 1

真实项目还应加入明确时区的事件发生时间、追踪标识等。不要随意把身份证号、银行卡号或密钥放进事件;历史回放会放大敏感数据传播范围。

运行项目

保持第 3 章 Broker 运行,并已按第 5 章创建 orders。启动两个新组:

bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group project-notify \
  --property print.key=true --property print.partition=true
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning --group project-stats \
  --property print.key=true --property print.partition=true

生产者命令与第 5 章相同,依次输入:

A2001|{"event_id":"e-101","order_id":"A2001","type":"OrderCreated","amount_fen":5800,"schema_version":1}
A2002|{"event_id":"e-102","order_id":"A2002","type":"OrderCreated","amount_fen":3900,"schema_version":1}
A2001|{"event_id":"e-103","order_id":"A2001","type":"OrderPaid","amount_fen":5800,"schema_version":1}

两个终端都应该包含这三个事件 ID。同一订单的两个事件应落入同一分区。消费者还可能显示前几章的历史数据,这是预期现象,按事件 ID 判断本次结果。

做三次故障练习

第一,暂停通知消费者,统计消费者保持运行,再发送一条新订单。统计仍能读到;通知组恢复后,应能读取暂停期间仍然保留的事件。由于停机前的提交时机不同,恢复时也可能包含重复。

第二,为通知组启动第二个实例,使用 kafka-consumer-groups.sh --describe 观察分区分配。停止其中一个后,再观察接管。

第三,停止两个通知实例,按第 7 章的方法对 project-notify 做回放。统计组不需要随之回放,因为组进度彼此独立。

项目验收表

这些命令验证了消息流动与组行为,没有实现真正的短信幂等和订单数据库事务。进阶时可选择一种熟悉的语言,把第 10 章的数据库去重流程做成小程序,再设计“业务成功后立即杀进程”的故障测试。

第 12 章 生产者与消费者怎样写得稳

生产者的五个关注点

第一,序列化:Kafka 保存字节,字符串和 JSON 需要编码;双方要约定字符集和字段格式。第二,Key 选择:它决定分区关联和热点风险。第三,可靠性:明确确认方式、幂等、超时与错误处理。第四,吞吐:批量与压缩能提高效率,但要评估延迟和 CPU。第五,关闭:正常退出时等待已发送任务完成,并处理失败。

batch.size 控制批次大小上限,linger.ms 影响等待聚合的时间,compression.type 选择压缩方式。它们需要用真实消息大小和流量验证,不是值越大越好。Kafka 4.0 起 Java 生产者的默认 linger.ms 调整为 5 毫秒,旧文章里“默认是 0”的结论不能直接套用到本书版本。

消费者的五个关注点

第一,配置正确的 group.id。第二,明确起点,auto.offset.reset=earliest 仅在没有有效进度时生效,不能覆盖已有书签。第三,决定提交时机。第四,控制批次处理时间,避免长时间不调用 poll 而触发组管理问题。第五,处理退出、再均衡和重试。

手动提交不是“关掉自动提交就万事大吉”。如果先把一批记录扔给线程池,马上提交整批进度,任务还没完成就宕机,仍会漏掉业务处理。

伪代码:至少一次处理

下面只是单线程、同步处理的思路,不是可以直接运行的语言代码:

关闭自动提交
循环:
    records = poll()
    按分区顺序处理本批记录
    每条记录:
        通过数据库事务与事件 ID 做幂等业务处理
        如果失败:进入既定重试策略,不跨过未完成记录提交
    本批各分区全部成功后:
        提交各分区“最后成功记录 offset + 1”

若批次中途失败,已成功但未提交的记录会重来,需要幂等。更细粒度提交可以降低重复范围,但会增加复杂度和开销。先把正确性做对,再优化。

数据格式演进

第一版有 order_id,第二版新增可选字段 coupon_id,通常比直接删除或改名旧字段容易兼容。消费者应明确必需字段、默认值、未知字段处理和错误策略。金额不能今天用元、明天无提示改成分。

Avro、Protobuf、JSON Schema 和 Schema Registry 是常见进阶工具;Schema Registry 并不是 Kafka Broker 自动附带的功能,兼容策略也需要团队约定。

第 13 章 运维入门:先看什么,再改什么

四类基本信号

服务可用性:Broker、Controller、客户端是否健康,是否有频繁断连。数据安全:副本是否同步、是否存在不可用分区、低于最小同步副本的情况。资源:磁盘剩余与增长、网络、CPU、内存和 GC。业务进度:Lag、端到端延迟、错误率和死信数量。

“CPU 不高”并不表示系统没有瓶颈。一个热点分区、外部数据库慢查询或磁盘写入限制,都可能拖慢处理。

Lag 一直上涨的排查顺序

先确认输入速率是否增加,再看消费速率是否下降;观察是否只有某个分区落后;检查错误重试、下游数据库、第三方 API 和再均衡;最后才判断是否需要增加消费者、分区或 Broker。

如果只有某个大客户占了绝大部分流量,且 Key 是客户号,新增消费者也可能无法分担那个热点分区。修改 Key 又可能影响顺序,所以需要在顺序要求与负载分摊之间设计。

常见故障对照

现象 优先检查 可以采取的下一步
连接 localhost:9092 失败 Broker 是否启动、端口、终端环境 看 Broker 日志,确认地址是否同一台机器
bootstrap 能连,后续连接失败 advertised.listeners 是否可被客户端访问 修正通告地址,特别注意容器与远程访问
提示 topic 已存在 是否重复执行创建 先 describe,确认后继续
消费没有输出 主题名、组进度、是否有新数据 换全新练习组从头读取
持续重复处理 提交是否成功、业务是否异常 检查处理与提交顺序、幂等凭证
NOT_ENOUGH_REPLICAS ISR 与 min.insync.replicas 修复副本健康,别先降低可靠性目标
磁盘快速增长 流量、保留、失衡与副本恢复 测算增长,规划容量和策略

生产环境边界

本书的 PLAINTEXT 本机单节点没有加密认证,也没有机器故障容错。生产部署需要按业务要求规划多节点与故障域、KRaft 仲裁、安全访问、容量、监控告警、升级和恢复演练。

TLS 保护传输,SASL 或双向 TLS 等机制提供身份验证,ACL 限制谁能读写哪些资源。只开放端口并不等于权限管理。安全配置应以对应版本官方文档为准,凭证不要写进书籍示例或提交到代码仓库。

第 14 章 Kafka Connect、Streams 与下一步

Connect:搬运数据

Kafka Connect 是 Kafka 生态中的数据集成框架。Source Connector 把外部系统的数据送进 Kafka,Sink Connector 把 Kafka 数据送到外部系统。例如数据库变更进入主题,主题数据写入分析仓库。

框架与连接器不是同一件事。不同连接器的安装、许可证、容错和投递语义不同。CDC 表示捕获数据变化,常用于数据库变更流;它仍要处理快照、表结构变化、权限和运维。

Streams:处理事件

Kafka Streams 是用于构建流处理应用的 Java/Scala 客户端库,不是必须额外搭建的独立 Kafka Broker。它可以做筛选、聚合、窗口计算和关联,应用本身仍需要部署、监控和扩容。

举例:统计每五分钟的订单金额,需要明确事件时间还是处理时间、窗口边界、迟到事件和重复事件如何处理。“五分钟统计”并不只是对数组求和那么简单。

四周学习安排

第一周完成第 1 至 5 章,每个概念配一个自己的例子。第二周完成组、进度和保留练习,记录重启和回放的结果。第三周精读可靠性与幂等,画出“业务成功、进度未提交”的故障时间线。第四周完成项目,选择熟悉的语言实现一个带数据库去重的消费者。

之后再研究多节点 KRaft、客户端编程、Connect、Streams、安全和容量。不要同时追逐所有参数;用一个真实问题推动下一次学习。

附录 A 常用命令速查

以下命令均在 Kafka 解压目录执行。请先启动本书 Broker。

查看主题列表:

bin/kafka-topics.sh --bootstrap-server localhost:9092 --list

查看订单主题:

bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders

查看消费者组列表和进度:

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group project-notify

查看主题级覆盖配置:

bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --entity-type topics --entity-name orders --describe

结束练习时,先在消费者和生产者终端按 Ctrl+C,再停止 Broker。需要再次学习时直接启动 config/book.properties,原数据和组进度仍可继续使用。

如果完全不再需要环境,确认所有进程已停,再通过文件管理器删除专门解压的练习目录。本文不提供自动递归清理命令,以免初学者在错误目录执行。

附录 B 词汇表

英文 中文 先记住这个意思
Event / Record 事件 / 记录 一条被写入的数据
Producer 生产者 把记录写进 Kafka 的应用
Consumer 消费者 从 Kafka 读取记录的应用
Topic 主题 一类记录的逻辑集合
Partition 分区 主题中的一条有序日志
Offset 偏移量 记录在分区里的位置
Broker 服务节点 运行 Kafka 数据服务的进程
Consumer Group 消费者组 协作读取并维护组进度的成员集合
Replica 副本 分区的一份数据复制
Leader / Follower 主副本 / 跟随副本 接受写入的副本及复制它的副本
ISR 同步副本集合 与 Leader 保持足够同步的副本
Rebalance 再均衡 调整组内的分区归属
Lag 消费落后量 消费进度与日志末端的距离指标
Retention 保留策略 决定旧数据何时被清理
Compaction 日志压缩清理 按 Key 在后台清理旧值
Idempotence 幂等 重复执行不重复产生业务效果
KRaft Kafka 元数据共识机制 通过 Controller 仲裁管理集群元数据
Outbox 事务发件箱 与业务事务一起写入的待发事件表

附录 C 综合练习与参考答案

1. 四个分区,同一组有六个消费者,会有多少消费者同时负责分区? 最多四个,具体取决于订阅和分配;其余成员可以空闲。

2. 同一主题有三个业务组,是否要把主题复制三份? 不需要。各组读取同一份保留数据,维护各自进度;读取压力仍需要容量规划。

3. 已处理 offset 9,应该提交多少? 若此前该分区的记录均已成功处理,通常提交 10,表示下一条的位置。

4. 重启旧组时加 --from-beginning,为什么没从头读取? 组有有效已提交进度,该参数不会覆盖它。可用新练习组验证历史,或停止原组后有计划地重置。

5. acks=all 是否意味着至少有三个副本? 不是。它等待当前 ISR,副本数量由主题等配置决定,还需要 min.insync.replicas 等配合。

6. 两条同一 Key 的消息是否一定按业务发生时间处理? 不能无条件保证。还要考虑上游并发、分区映射变化、消费者并发以及业务版本。

7. 业务数据库更新成功,提交进度前宕机,怎么处理? 允许再次读到,通过数据库事务中的去重凭证避免重复更新,再提交进度。

8. 保留七天,消费者停了十天,还能从原位置读完吗? 不能保证,旧数据可能已经清理。需要定义恢复点、备份或重新构建方案。

9. Kafka 事务能自动保证支付接口只扣一次吗? 不能。支付系统需要幂等键、结果查询或相应的业务协调机制。

10. Lag 增长就一定应该增加分区吗? 不一定。先排查输入增长、下游变慢、热点 Key、异常重试和再均衡,扩分区还会影响映射与顺序。

附录 D 资料来源与继续阅读

本书根据下列官方资料整理、解释并原创编写,不是官方文档逐字翻译。首次访问日期:2026-10-20,发布前于 2026-10-21 复核上述九个官方来源。版本路径固定在 4.1,页面内部可能更新补丁版本。阅读时请留意版本切换器,不要把不同大版本的默认值混在一起。

  1. Apache Kafka 4.1 Quick Start:压缩包、Java 要求、KRaft 初始化、启动及命令行消息练习。对应第 3 至 4 章。
  2. Introduction:事件、主题、分区、生产者与消费者。对应第 1 至 2 章。
  3. Design:日志、复制、投递语义、幂等、事务与消费位置。对应第 5、7、9 至 10 章。
  4. Producer Configs:acks、enable.idempotence、in-flight、批量和 linger.ms。对应第 9、12 章。
  5. Consumer Configs:group.id、auto.offset.reset、自动提交、poll 与组协议相关配置。对应第 6、7、12 章。
  6. Basic Kafka Operations:主题、配置、分区与管理操作。对应第 6 至 8、13 章和命令速查。
  7. Security Overview:传输加密、认证与授权入口。对应第 13 章。
  8. Kafka Streams:流处理库及后续教程入口。对应第 9、14 章。
  9. Kafka Connect Configs:Connect Worker 与连接器相关配置入口。对应第 14 章。

如果你只想再读一页官方材料,先读 Quick Start;如果想弄明白为什么会重复,读 Design 中的消息投递语义部分;如果要调参数,直接查对应版本的配置页,并通过自己的压测验证效果。