用订单事件学懂日志、分区与独立消费进度
2026-10-21
如果你刚听说 Kafka,却不知道“消息队列”“分区”“消费者”是什么意思,这本书就是为你准备的。你只需要会打开终端、复制命令,不需要先学分布式系统,也不必先写 Java。
读完并完成练习,你应该能够:说清 Kafka 解决什么问题;在自己的电脑上启动一个练习环境;发送和读取订单事件;观察同一份数据被不同业务独立读取;解释消息为什么可能重复、为什么重启后没有重新读取;知道生产环境还缺哪些条件。
本书将 Kafka 作为“可保留、可被多个业务独立读取的分区日志”来学习。若你已经读过 RocketMQ,这里的重点增量是日志位置与消费进度彼此独立:通知组可以暂停,统计组继续读取;通知组恢复或回放时,仍使用自己的书签。两种系统都支持消息与消费协作,选型应结合具体能力,不能仅凭“队列”和“日志”两个称呼下结论。
全书围绕三个问题推进:记录还在不在,由保留与压缩策略决定;某个业务读到哪里,由组偏移量决定;当前谁负责哪个分区,由组分配与再均衡决定。把这三条时间线分开,才能解释回放、扩缩容与故障重启。示例只观察真实 Kafka 单节点和命令行消费者,不冒充带数据库、短信或支付的完整应用。
本书是一条循序渐进的路线,不是参数百科。建议每天学习一到两章。第一次阅读先完成第 1 至 8 章,再学可靠性和项目。遇到名词可以翻到词汇表。
本书依据 Kafka 4.1 系列官方文档,示例固定到官方快速入门当前列出的 4.1.2,使用 KRaft,不需要 ZooKeeper。这是为了让命令有明确版本,不代表断言它是所有时间点的最新版本。官方网页会更新,资料访问日期为 2026 年 10 月 20 日。附录给出可直接打开的来源。
为了照顾初学者,正文把“消息”“事件”“记录”作为接近的说法。严格地说,事件强调“发生过的事实”,记录是
Kafka
实际保存的数据单位。代码块前面的解释会说明在哪里执行;复制命令时,不需要额外输入
$ 或 > 提示符。
你在购物网站按下“下单”。订单服务把订单写进数据库,随后还需要通知库存、发送短信、更新销售统计。最直接的做法是订单服务挨个调用这些系统。
这会产生两个问题。第一,慢系统拖累下单:短信服务卡了五秒,用户也可能等待五秒。第二,业务彼此绑得很紧:每增加一个新系统,订单服务都需要增加调用逻辑。
我们可以改成:订单服务记录“订单已经创建”这个事实,把它交给 Kafka;短信服务、统计服务分别从 Kafka 读取它。订单服务不用知道所有读者是谁。
订单服务 ──发送──> Kafka 的 orders 主题
├──> 通知服务
├──> 销售统计
└──> 风控分析
这叫作异步解耦。“异步”表示后续工作不必全部在同一次请求中结束;“解耦”表示系统减少对彼此实现细节和可用性的依赖。
假设平时每秒 100 个订单,促销时变成每秒 5,000 个。下游每秒只能处理 1,000 个,Kafka 可以暂时保存来不及处理的记录,让下游稍后继续。这叫削峰填谷。它不会凭空增加处理能力:流量长期高于消费能力时,积压仍然持续增长,最终会遇到存储或保留时间边界。
如果后来新增一个“订单地区分析”系统,它可以读取仍被保留的历史记录。这叫回放。已经被保留策略删除的数据不能靠修改消费进度找回来。
Kafka 是分布式事件流平台。现在先把它想象成一套“可多人订阅、按序追加、保存一段时间的日志”。日志在这里是一串数据记录,不只是程序报错文件。
Kafka 适合持续产生的大量事件、多个独立读者、需要历史回放的业务。它不自动替你完成库存事务、短信重试和订单查询。只需要少量后台任务时,更简单的任务队列也可能足够;按订单号查当前状态,通常仍然使用业务数据库。
本章自测:Kafka 能让慢消费者永远不扩容吗?不能。它能缓冲压力,不能消除长期的处理能力缺口。
生产者(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、Value、时间戳,也可以带 Headers。Key 可以是订单号,Value 可以是 JSON。固定分区数量、固定分区策略的前提下,同一个 Key 通常进入同一个分区,因此适合把同一订单的状态变化放在一起。增加分区或改变分区策略后,历史与新记录的映射可能变化。
消费者组(Consumer Group)是一组协作的消费者,使用相同的
group.id。同一组里,一个分区在稳定分配时交给一个消费者负责;不同组各自维护进度,都能独立读取同一份记录。
记住一句话:组内分工,组间独立。 三个分区、四个组内消费者,通常最多三个消费者同时获得分区;多出来的一个没有分区可读。
副本(Replica)是分区数据的复制。普通分区有一个 Leader 负责写入,Follower 从 Leader 复制。Leader 故障后,满足条件的副本可以接替。副本数和分区数不是同一个概念:三个分区、每分区三个副本,总计九份分区副本。
本章自测:两个业务都要完整读到订单,该用同一个组吗?不该。使用不同组;同一个组意味着协作分担。
本书统一采用官方压缩包与本地脚本,减少“主机地址”和“容器地址”混用。命令适用于 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/,不要直接把容器内路径与本地路径混用。
在新的终端,进入 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
并不是“强行覆盖已有消费进度”。在没有有效已提交进度时,它让消费者从最早的可用位置开始。已有组通常从保存的进度继续。需要从头演示时,最简单的方法是换一个新组;正式重放需要有计划地重置进度。
本章检查点:你应当能看到消息;能解释主题不会因消费而清空;能区分“新组从头读”和“旧组继续读”。
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
会怎样?所有这些消息会集中到同一个分区,失去按订单分摊流量的优势。
开两个消费者终端,使用不同组:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic orders --from-beginning --group notification-demobin/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,就能让三个分区快十倍吗?不能。先看分区并行度,再看真正的瓶颈。
消费者有“当前位置”,表示接下来取哪里;还有“已提交偏移量”,表示组恢复时从哪里继续。通常提交的是下一条要处理的记录位置。如果分区 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 进度之间缺少一个共同的原子提交边界。后文会讲幂等和事务。
上一章的组描述通常包含
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重启该组消费者,就会从最早保留位置继续。若对真实扣款业务回放,可能重新触发副作用,因此生产回放必须明确目标范围并先确保幂等。
重置成功只说明书签被修改,还应重新启动同一个组,实际核对三条中文记录确实再次读到;同时比较另一业务组的已提交位置,确认没有被一起重置。完整回放方案需要记录目标主题和分区、起止位置、操作者、业务幂等依据以及停止条件。需要独立重算又不想修改原组时,可以用新的组读取保留日志,但新增读者仍会消耗集群资源。
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 与协议开销、索引、峰值、故障副本恢复和磁盘余量。别把这道估算题当成采购结论。监控磁盘增长速度,比只看配置更可靠。
本章自测:某组把所有消息读完,会释放主题磁盘吗?不会。删除主要由保留、压缩或显式管理操作决定。
生产者返回成功、Broker 保存记录、所有副本完成复制、消费者读到、业务数据库更新成功,是不同阶段。讨论“不丢”或“不重复”时,必须说明在哪个阶段、容忍哪些故障。
生产者发出请求但没有收到回应,也不等于 Broker 没收到:请求可能成功了,只是响应在网络里丢了。这也是重试可能制造重复的原因。
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
也提供相应的处理保证。普通数据库更新或外部支付接口不自动包含在这个事务里。
本章自测:设置生产者幂等后,短信还能重复吗?能。短信副作用与消费进度之间仍需业务设计。
本章提供数据库幂等与 Outbox 的设计推导,没有附带数据库、CDC 转发器或真实支付调用。第 11 章的 CLI 项目验证日志和消费进度,不是本章事务设计的端到端实现。Kafka 事务的原理说明也不代表本轮做过跨系统事务实验。
幂等的意思是:同一个业务操作执行多次,最终有效结果与执行一次相同。例如对
event_id=e-001 的积分增加只能生效一次。
常见做法是在数据库中建立处理记录表,以
(consumer_name, event_id)
为唯一键。消费者在同一个数据库事务中插入去重记录并更新业务表。唯一键冲突说明这一业务消费者已处理过该事件,可以跳过业务更新。
读取事件
↓
开始数据库事务
├─ 插入处理凭证(唯一约束)
└─ 执行业务更新
提交数据库事务
↓
提交 Kafka 消费进度
若数据库事务成功、Kafka 进度未提交,重启后会再次读到;数据库唯一约束让第二次更新失效。若业务事务失败,不能提交进度假装成功。
“先查没有,再插入”会有并发竞争,必须由数据库唯一约束和事务保证正确性。去重记录保留期还要覆盖可能重放的历史,否则删除去重凭证后,老事件可能再次生效。
外部发短信或支付不能和普通本地数据库事务自然原子化。优先使用对方提供的幂等请求号,或把待执行任务写入本地发件箱,再由独立执行器按幂等规则调用并核对结果。
订单服务面临另一个问题:先写订单库,再发 Kafka,两步之间可能宕机;先发 Kafka,再写库,也可能产生没有真实订单的事件。
事务发件箱(Transactional Outbox)的思路是,在同一个数据库事务里写订单表与 outbox 表。后台转发器或 CDC 工具再把 outbox 的事件送进 Kafka。转发也可能重复,因此消费者仍应幂等。
这提供的是可恢复的协调方式,需要监控未转发事件、重试、顺序和清理,不等于“装一个插件就自动解决所有一致性问题”。
短暂网络失败可以有限重试并退避;字段格式错误反复重试通常不会自己变好。达到边界后,可以送入死信主题,记录原主题、分区、偏移量、事件 ID、错误原因和失败时间。
死信需要告警、责任人、修复和重放流程。若把同一订单的失败事件移走后继续处理后续事件,业务顺序可能改变。把它作为明确的业务决策,而不是默认“跳过就好”。
现在你要完成一个“订单事件观察站”:订单生产者写入
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=truebin/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
做回放。统计组不需要随之回放,因为组进度彼此独立。
e-101、e-102、e-103。这些命令验证了消息流动与组行为,没有实现真正的短信幂等和订单数据库事务。进阶时可选择一种熟悉的语言,把第 10 章的数据库去重流程做成小程序,再设计“业务成功后立即杀进程”的故障测试。
第一,序列化: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 自动附带的功能,兼容策略也需要团队约定。
服务可用性:Broker、Controller、客户端是否健康,是否有频繁断连。数据安全:副本是否同步、是否存在不可用分区、低于最小同步副本的情况。资源:磁盘剩余与增长、网络、CPU、内存和 GC。业务进度:Lag、端到端延迟、错误率和死信数量。
“CPU 不高”并不表示系统没有瓶颈。一个热点分区、外部数据库慢查询或磁盘写入限制,都可能拖慢处理。
先确认输入速率是否增加,再看消费速率是否下降;观察是否只有某个分区落后;检查错误重试、下游数据库、第三方 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 限制谁能读写哪些资源。只开放端口并不等于权限管理。安全配置应以对应版本官方文档为准,凭证不要写进书籍示例或提交到代码仓库。
Kafka Connect 是 Kafka 生态中的数据集成框架。Source Connector 把外部系统的数据送进 Kafka,Sink Connector 把 Kafka 数据送到外部系统。例如数据库变更进入主题,主题数据写入分析仓库。
框架与连接器不是同一件事。不同连接器的安装、许可证、容错和投递语义不同。CDC 表示捕获数据变化,常用于数据库变更流;它仍要处理快照、表结构变化、权限和运维。
Kafka Streams 是用于构建流处理应用的 Java/Scala 客户端库,不是必须额外搭建的独立 Kafka Broker。它可以做筛选、聚合、窗口计算和关联,应用本身仍需要部署、监控和扩容。
举例:统计每五分钟的订单金额,需要明确事件时间还是处理时间、窗口边界、迟到事件和重复事件如何处理。“五分钟统计”并不只是对数组求和那么简单。
第一周完成第 1 至 5 章,每个概念配一个自己的例子。第二周完成组、进度和保留练习,记录重启和回放的结果。第三周精读可靠性与幂等,画出“业务成功、进度未提交”的故障时间线。第四周完成项目,选择熟悉的语言实现一个带数据库去重的消费者。
之后再研究多节点 KRaft、客户端编程、Connect、Streams、安全和容量。不要同时追逐所有参数;用一个真实问题推动下一次学习。
以下命令均在 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,原数据和组进度仍可继续使用。
如果完全不再需要环境,确认所有进程已停,再通过文件管理器删除专门解压的练习目录。本文不提供自动递归清理命令,以免初学者在错误目录执行。
| 英文 | 中文 | 先记住这个意思 |
|---|---|---|
| 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 | 事务发件箱 | 与业务事务一起写入的待发事件表 |
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、异常重试和再均衡,扩分区还会影响映射与顺序。
本书根据下列官方资料整理、解释并原创编写,不是官方文档逐字翻译。首次访问日期:2026-10-20,发布前于 2026-10-21 复核上述九个官方来源。版本路径固定在 4.1,页面内部可能更新补丁版本。阅读时请留意版本切换器,不要把不同大版本的默认值混在一起。
如果你只想再读一页官方材料,先读 Quick Start;如果想弄明白为什么会重复,读 Design 中的消息投递语义部分;如果要调参数,直接查对应版本的配置页,并通过自己的压测验证效果。