👋 关于作者
你好,这里是一条泥憨鱼 | 专注于 后端开发、AI 前沿技术与计算机基础 。

🏠 个人主页:一条泥憨鱼
通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!
如果你第一次接触 Kafka,很容易被 Topic、Partition、Offset、Consumer Group、ISR 这一串名词劝退。其实它的核心很简单:
Kafka 是一个只能追加、可以重复读的分布式日志。生产者往里写,消费者按自己的进度读,消息按策略保留,而不是读完就删。
先建立这个印象,后面的概念都是它的展开。
全文较长,建议先收藏,再按下方「怎么读」里的路线选择阅读方式。
写在前面
先看三句话
- Kafka 是一个「只能追加、可以重复读」的分布式日志,不是传统意义上「读完就删」的队列。
- 生产者写进去,消费者按自己的进度读。每个消费组自己记一个 offset(读到哪了),互不干扰。
- Kafka 快,是因为顺序写磁盘 + 批量 + 零拷贝;Kafka 难,主要难在「分区、副本、消费组」这几个概念和运维。
版本与环境
| 组件 | 本文使用版本 |
|---|---|
| Apache Kafka | 4.4.0(KRaft 模式,无需 ZooKeeper) |
| Spring Boot | 4.1.2 |
| JDK | 17+ |
| Maven | 3.9+ |
| 运行环境 | Docker Desktop;命令以 Windows + PowerShell 为例 |
Linux / macOS 用户也能跟着做,只需注意几处命令差异:
curl.exe换成curl;Test-NetConnection localhost -Port 9092换成nc -zv localhost 9092;Get-NetTCPConnection -LocalPort 9092换成lsof -i :9092。
版本提示 :本文基于 Kafka 4.x 和 Spring Boot 4.x。如果你用的是 Spring Boot 3.x,依赖要改成
org.springframework.kafka:spring-kafka,JSON 序列化类用JsonSerializer/JsonDeserializer。核心概念和 Kafka 命令行基本一致。
怎么读
| 阅读方式 | 建议路线 | 时间 |
|---|---|---|
| 30 分钟速通 | 1.3~1.4 起服务 → 2.1~2.5 核心模型 → 3.1~3.4 收发消息 → 5.4 选型对比 | 30 分钟 |
| 完整入门 | 按顺序读完,并动手跑一遍 | 4~6 小时 |
| 当手册查 | 直接看第三、六章 | 随时 |
全文结构
| 部分 | 你会学到 |
|---|---|
| 一、5 分钟跑起来 | 用 Docker 起单机 Kafka、常用运维命令、advertised.listeners 怎么配 |
| 二、核心概念 | Topic / Partition / Offset、副本与 ISR、消费组、投递语义、顺序性 |
| 三、命令行操作 | 建 topic、收发消息、查看 LAG、重置 offset、修改保留策略 |
| 四、Spring Boot 实战 | 完整可运行的生产者 + 消费者 + REST 接口,以及手动提交、JSON、并发等进阶用法 |
| 五、优缺点与选型 | Kafka 的边界,和 RabbitMQ / RocketMQ / Pulsar 怎么选 |
| 六、踩坑面试进阶 | 故障排查、16 道高频面试题、生产检查清单、四周学习路线 |
一、5 分钟把 Kafka 跑起来
目标:用 Docker 在本机起一个单机 Kafka,能建 topic、能收发消息。学完这一章,你就有了一个随时可以折腾的实验环境。
1.1 为什么用 Docker,而不是 Windows 原生安装
- Kafka 官方不推荐在 Windows 上直接跑生产环境,原生安装容易在路径、脚本、权限上踩坑。
- Kafka 4.x 已经彻底移除 ZooKeeper,单机模式只需要一个进程(KRaft 模式),Docker 一条命令就能起。
- 用 Docker 的好处:删了重来只要几秒,不污染系统;换电脑时复制一个 yml 文件即可。
Windows 上建议启用 WSL2(wsl -l -v 能看到发行版),Docker Desktop 正好用 WSL2 作为后端,不用额外装虚拟机。macOS / Linux 直接用各自的 Docker 引擎。
1.2 安装 Docker Desktop
已经装好 Docker 的话,终端里
docker version能同时输出 Client 和 Server 两段,就说明装好了,直接跳到 1.3。
第一步:确认 WSL2 正常。
powershell
wsl --status
wsl --update
如果提示需要重启,就重启一次。
第二步:安装 Docker Desktop。
powershell
winget install -e --id Docker.DockerDesktop
安装完成后注销并重新登录 Windows(或者重启),让 docker 命令进入 PATH。
想把 Docker 装到非系统盘,可以改用官方安装器参数:
powershell"Docker Desktop Installer.exe" install --quiet --accept-license --backend=wsl-2 ` --installation-dir=D:\Docker ` --wsl-default-data-root=D:\docker-data前一个参数控制程序目录,后一个控制镜像和 WSL 磁盘位置。装完后如果
docker compose提示找不到命令,在%USERPROFILE%\.docker\config.json里加上:
json"cliPluginsExtraDirs": ["D:\\Docker\\resources\\cli-plugins"]
第三步:启动并检查。
从开始菜单打开 Docker Desktop:首次启动接受协议,可以跳过登录,左下角显示 Engine running 才算启动完成。
新开一个 PowerShell 窗口验证:
powershell
docker --version
docker version
docker version 能同时显示 Client 和 Server 两段信息,才算真的连上了 Docker 引擎。只有 Client 没有 Server,说明 Docker Desktop 还没启动。
国内网络提示 :拉取
apache/kafka镜像可能很慢或超时。可以在 Docker Desktop → Settings → Docker Engine 里配置镜像加速或代理。2024 年以后大量公共加速站已关停,最稳的方式通常是公司或学校代理。镜像有几百 MB,配不上也别慌,多等一会儿通常能拉下来。
1.3 启动 Kafka
新建一个目录,放入下面这个 docker-compose.yml:
yaml
# 最简单机 Kafka(KRaft 模式,无需 ZooKeeper)
# 启动:docker compose up -d
# 停止并删除:docker compose down
services:
kafka:
image: apache/kafka:4.4.0
container_name: kafka
ports:
- "9092:9092"
restart: unless-stopped
在该目录下执行:
powershell
docker compose up -d
第一次会下载 apache/kafka:4.4.0 镜像,耐心等。看到类似输出就成功了:
[+] Running 2/2
✔ Network demo_default Created
✔ Container kafka Started
检查容器状态:
powershell
docker compose ps
STATUS 应该是 Up ...。再看日志确认 Kafka 真的启动了:
powershell
docker compose logs kafka
在输出里找这一行:
Kafka Server started
单机模式下,同一个进程既是 broker 又是 controller(KRaft 的 combined 模式),所以只有一个容器、没有 ZooKeeper。官方镜像自带一套「单机 KRaft 可用」的默认配置,因此只要映射 9092 端口就能用。学习阶段这样就够了。
1.4 验证:第一条 Kafka 命令
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
- 没有任何输出、也没有报错 = 成功。说明 Kafka 正常运行,当前还没有 topic。
- 如果报
Connection to node -1 could not be established,回到 1.3 看容器是否 Up、日志里有没有Kafka Server started。
再确认一下版本:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --version
应该显示 4.4.0。
容器里的 Kafka 命令都在
/opt/kafka/bin/下,所以每条命令都要带这个前缀。嫌长可以在当前 PowerShell 窗口定义一个函数:
powershellfunction kfk-topics { docker exec -it kafka /opt/kafka/bin/kafka-topics.sh @args }之后就可以写
kfk-topics --bootstrap-server localhost:9092 --list。
1.5 常用运维命令
powershell
# 启动(后台)
docker compose up -d
# 看状态
docker compose ps
# 看日志(Ctrl+C 退出查看,不会停容器)
docker compose logs -f kafka
# 停止并删除容器(数据会丢,学习环境无所谓)
docker compose down
# 只停止,不删除
docker compose stop
# 重新启动
docker compose start
最简版没有挂数据卷,docker compose down 之后消息就没了。学习阶段这是好事,每次从干净状态开始。想要数据保留,用下面的 full 版。
安全提示 :不要随便在 Docker Hub 页面上点「Run」。这个按钮会直接拉取并运行镜像里的程序,镜像作者可以放任何东西(挖矿、后门、反弹 shell)。只运行官方镜像(
apache/、library/、confluentinc/等)或你明确信任的镜像。
1.6 进阶:显式配置 + 数据卷
等你看完第二章,可以换成下面这份 docker-compose.full.yml。它把关键参数都写出来了,更适合理解:
yaml
# 显式配置版:把关键参数写清楚,并挂载数据卷做持久化
# 启动:docker compose -f docker-compose.full.yml up -d
# 停止:docker compose -f docker-compose.full.yml down
services:
kafka:
image: apache/kafka:4.4.0
container_name: kafka
hostname: kafka
ports:
- "9092:9092"
environment:
# ---- 节点与角色 ----
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
CLUSTER_ID: "5L6g3nShT-eMCtK--X86sw"
# ---- 监听地址 ----
# LISTENERS:进程实际监听的地址
# ADVERTISED_LISTENERS:告诉客户端用哪个地址回来连
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
# ---- KRaft 控制器投票节点 ----
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
# ---- 单机必须把内部 topic 的副本数改成 1 ----
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
# ---- 数据目录(与下面的 volume 保持一致)----
KAFKA_LOG_DIRS: /var/lib/kafka/data
# ---- 开发环境方便起见,允许自动建 topic ----
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
volumes:
- kafka-data:/var/lib/kafka/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list >/dev/null 2>&1"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
volumes:
kafka-data:
核心配置逐条解释:
| 配置 | 作用 |
|---|---|
KAFKA_NODE_ID: 1 |
节点编号,集群内唯一 |
KAFKA_PROCESS_ROLES: broker,controller |
同时承担 broker 和 controller(单机合并模式) |
KAFKA_LISTENERS |
进程监听哪些地址和端口 |
KAFKA_ADVERTISED_LISTENERS |
告诉客户端「你应该用这个地址来找我」 |
KAFKA_CONTROLLER_QUORUM_VOTERS |
KRaft 控制器选举的投票节点列表 |
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 |
单机必须改成 1,否则内部 topic 副本数为 3 会建不出来 |
KAFKA_LOG_DIRS |
数据目录,配合 volume 做持久化 |
ADVERTISED_LISTENERS 是 Kafka 最容易配错的地方,记住一句话:
客户端连上 broker 后,broker 会返回
advertised.listeners里的地址让客户端再去连。这个地址必须是客户端视角下能访问的地址 。本机客户端就写localhost:9092;如果客户端是另一个容器,就要写容器网络里的服务名,比如kafka:9092。
配错的典型症状:能建立连接,但发消息一直超时或重试。
1.7 排错清单
| 现象 | 原因 / 解决 |
|---|---|
docker 不是内部或外部命令 |
装完没重开终端或没注销;重开 PowerShell |
docker version 只有 Client |
Docker Desktop 没启动,或 Engine 还在 starting |
wsl 报错 / Docker 起不来 |
wsl --update,然后在 BIOS 里确认虚拟化已开启 |
docker compose up 卡在 pull |
网络问题,配代理或镜像;确认 docker pull apache/kafka:4.4.0 能单独成功 |
| 容器启动后马上 Exited | docker compose logs kafka 看报错;常见是端口占用或配置项拼写错误 |
| 9092 端口被占用 | Get-NetTCPConnection -LocalPort 9092 查进程;或把 compose 改成 "19092:9092",客户端连 localhost:19092 |
kafka-topics.sh 连不上 |
等 10~30 秒再试;确认容器 Up;确认用了 --bootstrap-server localhost:9092 |
docker compose down 后消息没了 |
正常,最简版没挂数据卷;需要保留就用 full 版 |
二、核心概念:看懂 Kafka
概念顺了,后面的命令和代码都是自然结果。
2.1 定义
Kafka 是一个分布式的、只能追加的日志(append-only log),生产者往里写,消费者按自己的进度读,消息按策略保留而不是读完就删。
三个关键词:
- 分布式:可以多台机器组成集群,水平扩展。
- 只能追加:消息一旦写入就不可修改,只能往后加。
- 日志:像一本只往后写的账本,每行有一个递增编号。
所以 Kafka 常被叫做「分布式提交日志」或「事件流平台」,而不只是「消息队列」。
传统队列像传送带,东西被取走就没了。Kafka 像银行流水账,每笔交易按顺序记下来,谁想看都可以从任意一页开始看,账本按时间保留,不会因为有人看过就撕掉。
2.2 看懂整体结构
┌──────────────────── Kafka 集群 ────────────────────┐
│ │
Producer ──写──▶│ Topic: orders │
(生产者) │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │Partition 0 │ │Partition 1 │ │Partition 2 │ │
│ │ 0 1 2 3 4..│ │ 0 1 2 3 4..│ │ 0 1 2 3 4..│ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ ▲ ▲ ▲ │
└─────────┼──────────────┼──────────────┼──────────────┘
│ │ │
┌─────────────┴───┐ ┌──────┴──────────┐ │
│ 消费组 A │ │ 消费组 B │ │
│ offset: 3 │ │ offset: 5 │ │
│ (自己记自己的) │ │ (互不影响) │ │
└─────────────────┘ └─────────────────┘ │
记住这张图的四个角色:Producer、Topic/Partition、Consumer Group、Broker。
如果你用过 RabbitMQ,可以这样对照:RabbitMQ 的核心是「队列 + Exchange 路由」,Kafka 的核心是「分区 + offset」,消息不是被确认后删除,而是按保留策略留在日志里。两者不是替代关系,选型部分会细说。
2.3 一条消息长什么样
一条 Kafka 消息本质上是一个键值对,外加一些元数据:
| 字段 | 说明 |
|---|---|
key |
可选。决定消息进哪个分区,也常用于业务主键(如订单号、用户 ID) |
value |
消息正文,通常是 JSON / Avro / Protobuf 序列化后的字节 |
timestamp |
生产时间或写入时间 |
headers |
可选的自定义头,类似 HTTP header |
partition / offset |
写入后由 Kafka 分配,标识它落在哪、是第几条 |
Kafka 本身不关心 value 里是什么,它只存字节。序列化和反序列化是客户端的事。
2.4 Topic 和 Partition
Topic 是逻辑上的消息分类,类似数据库里的「表」或 RabbitMQ 里的「队列名」。比如:
orders:订单事件payments:支付事件user-behavior:用户行为日志
一个 Topic 在物理上被切成多个 Partition(分区)。每个分区是一个独立的、有序的、只能追加的日志文件。
分区带来三个好处:
- 水平扩展:不同分区可以放在不同 broker 上,吞吐随机器增加。
- 并行消费:一个消费组内,不同消费者可以同时消费不同分区。
- 局部有序:同一分区内严格有序。
代价也要知道:
- 分区数不是越多越好。分区多了会增加 broker 的元数据、文件句柄、选举和 rebalance 成本。
- 单机学习环境用 1~3 个分区足够;生产环境常见每个 topic 6~48 个分区,按吞吐和消费者数量定。
- 分区数只能增加,不能减少(减少会丢消息),所以创建时要稍微留余量。
2.5 Offset:消息在分区里的「门牌号」
每个分区内部,消息从 0 开始编号,这个编号就是 offset。
Partition 0:
offset: 0 1 2 3 4 5
┌────┬─────┬─────┬─────┬─────┬─────┐
│msg │ msg │ msg │ msg │ msg │ ... │
└────┴─────┴─────┴─────┴─────┴─────┘
▲
消费者下次读这里(offset=3)
两个容易混的概念:
- 消费位置(position):消费者当前读到哪,存在内存里。
- 提交的 offset(committed offset) :消费者告诉 Kafka「我已经处理到哪了」,存在 Kafka 内部的
__consumer_offsetstopic 里。
committed offset 指的是「下一条要读的消息」,不是「最后一条读过的消息」。所以提交 offset=3,表示 0、1、2 都处理完了,下次从 3 开始读。
offset 只在分区内有意义。Partition 0 的 offset=5 和 Partition 1 的 offset=5 是两条完全不同的消息。
2.6 Broker、Controller 和 KRaft
- Broker:一个 Kafka 服务进程。一个 broker 可以管理多个分区。3 台机器组成集群,就是 3 个 broker。
- Controller:集群里负责管理元数据(topic、分区、副本、leader 选举)的角色。以前由 ZooKeeper 承担,现在由 Kafka 自己选出一个 controller。
- KRaft :Kafka 自己实现的 Raft 一致性协议,用来替代 ZooKeeper。Kafka 4.0 起 ZooKeeper 已被彻底移除,所有集群都跑在 KRaft 上。
单机学习环境通常是「combined 模式」:一个进程同时是 broker 和 controller。生产环境一般把 controller 和 broker 分开部署。
2.7 副本(Replica)与 ISR:高可用的基础
每个分区可以有多个副本,分散在不同 broker 上:
Partition 0, replication-factor = 3
Broker 1: [Leader] ← 生产和消费都找它
Broker 2: [Follower] ← 持续从 Leader 拉数据
Broker 3: [Follower]
- Leader:唯一对外提供读写的副本。
- Follower:只负责同步,不对外服务(除非发生故障切换)。
- ISR(In-Sync Replicas):当前与 leader 保持同步的副本集合。落后太多的 follower 会被踢出 ISR。
关键参数:
| 参数 | 含义 |
|---|---|
replication.factor |
每个分区总共几个副本,生产环境通常 3 |
min.insync.replicas |
至少几个副本确认才算写入成功,通常 2 |
acks=all |
生产者要求所有 ISR 都确认(Kafka 3.0+ 默认) |
三者配合才能在坏一台机器时不丢数据:
replication.factor=3+min.insync.replicas=2+acks=all:最多允许坏 1 台,写入仍然安全。
单机学习环境副本数只能是 1,所以没有高可用可言,broker 挂了数据就没了。这是正常的,别在单机上纠结副本。
2.8 生产者(Producer)怎么工作
生产者发送一条消息,大致经历:
- 序列化:把对象变成字节(String、JSON、Avro...)。
- 选分区 :
- 指定了 partition → 直接用;
- 有 key → 对 key 做哈希,再对分区数取模;
- 没有 key → 粘性分区(一段时间内集中写一个分区,攒批更高效)。
- 攒批 :消息先进入客户端缓冲区,凑够
batch.size或等够linger.ms再一起发。 - 发送并等待确认 :按
acks决定等谁确认。 - 失败重试 :按
retries重试,配合幂等性避免重复。
常用参数(初学先知道有这些):
| 参数 | 默认值(Kafka 4.x) | 作用 |
|---|---|---|
acks |
all |
0=不等确认,1=leader 确认,all=ISR 全部确认 |
enable.idempotence |
true |
幂等生产者,避免重试导致重复 |
linger.ms |
5 | 最多等多久攒批,调大吞吐高但延迟高 |
batch.size |
16KB | 单批最大字节数 |
compression.type |
none |
压缩算法,常用 lz4 / zstd |
吞吐和延迟是一对矛盾 :
linger.ms越大,批越大、吞吐越高,但单条消息等待越久。默认值已经是折中。
2.9 消费者与消费组(Consumer Group)
这是 Kafka 最核心、也最容易搞混的部分。
基本规则:
- 消费者用
group.id表明自己属于哪个消费组。 - 同一个消费组内,一个分区最多分配给一个消费者。
- 不同消费组之间完全独立,各自维护自己的 offset,同一条消息可以被多个组各读一次。
举例。 Topic 有 3 个分区:
消费组 A(2 个消费者):
consumer-1 → Partition 0, Partition 1
consumer-2 → Partition 2
消费组 B(1 个消费者):
consumer-3 → Partition 0, Partition 1, Partition 2
消费组 C(4 个消费者):
consumer-4 → Partition 0
consumer-5 → Partition 1
consumer-6 → Partition 2
consumer-7 → 空闲(没有分区可分)
由此得出两个重要结论:
- 消费者数量超过分区数,多出来的消费者会闲着。想提高消费并行度,先加分区。
- 同一个分区不会被同组两个消费者同时消费,所以组内不会重复消费(正常情况)。
拉模型(Pull)。 Kafka 消费者是主动拉消息的,不是 broker 推。好处是消费者能自己控制节奏,不会被压垮;坏处是空闲时会有一点点轮询开销(Kafka 用长轮询解决)。
Rebalance(重平衡)。 以下情况会让消费组重新分配分区:
- 消费者加入或离开;
- 消费者心跳超时(被认为挂了);
- 订阅的 topic 分区数变化。
重平衡期间消费会短暂停顿,生产环境要尽量避免频繁 rebalance。常见原因:消费者处理一条消息太慢,超过 max.poll.interval.ms(默认 5 分钟)被踢出组。
Offset 提交与重复消费。
- 默认
enable.auto.commit=true,每 5 秒自动提交一次。 - 如果消息处理到一半消费者崩了,已提交的 offset 可能已经越过未处理完的消息,导致消息丢失(at-most-once 风险)。
- 如果先处理业务、再提交 offset,崩在提交前,导致消息重复(at-least-once)。
所以 Kafka 默认语义通常是 at-least-once(至少一次),业务要自己做幂等。
2.10 消息保留:读完不会删
这是 Kafka 和传统队列最大的区别。
Kafka 的消息不会因为被消费而删除,而是按配置保留:
| 配置 | 默认 | 含义 |
|---|---|---|
log.retention.hours |
168(7 天) | 超过 7 天的消息删除 |
log.retention.bytes |
-1(不限) | 分区总大小超过阈值后删旧消息 |
cleanup.policy |
delete |
也可设为 compact,按 key 保留最新值 |
由此带来几个能力:
- 重放(replay):把 offset 重置回更早的位置,重新消费历史消息。
- 多消费组独立读:新上线的服务可以从头读历史数据。
- 事件溯源 / 流处理:把 Kafka 当成可回放的事件存储。
代价是存储成本:消息留得越久、流量越大,磁盘占用越高。
2.11 三种投递语义
| 语义 | 做法 | 风险 |
|---|---|---|
| At most once(至多一次) | 先提交 offset,再处理 | 可能丢消息 |
| At least once(至少一次) | 先处理,再提交 offset | 可能重复(最常见) |
| Exactly once(精确一次) | 幂等生产者 + 事务 + read_committed |
配置复杂,只覆盖 Kafka 内部读写 |
现实建议:
- 绝大多数业务用 at-least-once + 消费端幂等(比如用业务唯一 ID 去重)。
- 「精确一次」只在 Kafka 的读-处理-写链路内成立;如果消费时要调外部接口、写数据库,仍然要靠业务幂等或事务消息方案。
2.12 顺序性:只在分区内保证
Kafka 不保证全局顺序 ,只保证单个分区内的顺序。
想保证同一业务实体的消息有序:
- 用业务主键作为消息
key(如orderId); - Kafka 会对 key 哈希,同一个 key 永远进同一个分区;
- 该分区内按写入顺序消费。
注意:
- 分区数一旦变化,key 到分区的映射会变,历史消息和新消息可能不在同一分区,顺序假设会被打破。
- 全局有序只能把 topic 设成 1 个分区,吞吐会严重受限,绝大多数场景不需要。
2.13 术语速查表
| 术语 | 一句话解释 |
|---|---|
| Broker | 一个 Kafka 服务节点 |
| Controller | 管理集群元数据和 leader 选举的角色 |
| KRaft | Kafka 自带的共识协议,替代 ZooKeeper |
| Topic | 消息的逻辑分类 |
| Partition | Topic 的物理分片,有序追加日志 |
| Offset | 消息在分区内的递增编号 |
| Replica | 分区的副本 |
| Leader / Follower | 副本中的主 / 从 |
| ISR | 与 leader 保持同步的副本集合 |
| Producer | 消息生产者 |
| Consumer | 消息消费者 |
| Consumer Group | 一组协同消费的消费者,组内分区不重复分配 |
| Rebalance | 消费组重新分配分区 |
| Lag | 消费进度落后量 = 最新 offset − 已提交 offset |
| Retention | 消息保留策略 |
| Compaction | 按 key 压缩,只保留每个 key 的最新值 |
2.14 五个常见误解
-
「Kafka 消息消费完就删了」 ❌
错。按保留策略删除,和消费无关。这正是它能重放的原因。
-
「分区越多越好」 ❌
分区有元数据、文件句柄、rebalance、端到端延迟等成本。够用就好。
-
「Kafka 能保证全局顺序」 ❌
只保证单分区内有序。要业务有序就用 key。
-
「消费者加得越多消费越快」 ❌
同组内并行度上限 = 分区数。消费者多于分区,多出来的空闲。
-
「Kafka 就是个消息队列」 ⚠️
它同时是消息队列、事件存储和流处理平台(Kafka Streams / Connect)。把它只当队列用,会浪费它的核心能力。
三、命令行操作:把 Kafka 玩起来
这一章全是命令,建议边看边敲。前提是第一章的容器已经启动。
约定:容器名 kafka,Kafka 命令目录 /opt/kafka/bin/,broker 地址 localhost:9092。每条命令的完整形式是:
powershell
docker exec -it kafka /opt/kafka/bin/<脚本> --bootstrap-server localhost:9092 ...
3.1 先认识几个脚本
| 脚本 | 用途 |
|---|---|
kafka-topics.sh |
建 / 删 / 改 / 查 topic |
kafka-console-producer.sh |
命令行发消息 |
kafka-console-consumer.sh |
命令行收消息 |
kafka-consumer-groups.sh |
查消费组、LAG、重置 offset |
kafka-configs.sh |
改 topic / broker 配置 |
kafka-get-offsets.sh |
查分区最新 offset |
Kafka 4.x 的命令统一用
--bootstrap-server,不再支持--zookeeper(ZooKeeper 已被移除)。
3.2 Topic 的增删改查
创建 topic:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic demo-topic --partitions 3 --replication-factor 1
参数说明:--partitions 3 是 3 个分区;--replication-factor 1 是因为单机只能 1 个副本。
成功输出:
Created topic demo-topic.
查看所有 topic:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
查看某个 topic 的详情:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic demo-topic
输出类似:
Topic: demo-topic TopicId: xxxx PartitionCount: 3 ReplicationFactor: 1 Configs:
Topic: demo-topic Partition: 0 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr:
Topic: demo-topic Partition: 1 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr:
Topic: demo-topic Partition: 2 Leader: 1 Replicas: 1 Isr: 1 Elr: LastKnownElr:
Leader:负责这个分区的 broker id(单机就是 1);Replicas:副本所在 broker;Isr:当前同步的副本。
增加分区(只能加,不能减):
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic demo-topic --partitions 6
生产环境慎用:分区数变化会影响按 key 的哈希映射,可能破坏「同一 key 顺序消费」的假设。
删除 topic:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic demo-topic
3.3 发消息:console producer
打开一个终端执行:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic demo-topic --property parse.key=true --property key.separator=:
出现 > 提示符后,一行一条消息,格式是 key:value:
user-1:第一条消息
user-2:第二条消息
user-1:第三条消息
按 Ctrl+C 退出。
不带
parse.key=true时,整行都会当成 value,没有 key。有 key 才能控制分区。
3.4 收消息:console consumer
从最早的消息开始读。 新开一个终端:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic demo-topic --from-beginning --property print.key=true --property print.partition=true --property print.offset=true
你应该能看到刚才发的三条消息,并且输出里会带上每条消息的 key、partition 和 offset。不同 Kafka 版本、不同格式化器的排版可能略有差异,不用纠结列的顺序,重点观察:同一个 key 的消息是否落在同一个 partition。
带消费组读(会记录 offset):
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic demo-topic --group g1 --property print.key=true --property print.partition=true
要点:
- 加了
--group才会提交 offset,下次再启动会从上次位置继续。 --from-beginning只在这个组还没有已提交 offset 时生效。组一旦有了 offset,就按 offset 继续读。
再开一个不同组的消费者:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic demo-topic --group g2 --from-beginning
你会看到 g2 把历史消息又完整读了一遍。不同消费组互不影响,这就是 Kafka 多订阅者的方式。
同一个组开两个消费者。 再开一个终端,仍然用 --group g1:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic demo-topic --group g1 --property print.partition=true
两个 g1 消费者会各自分到一部分分区(3 个分区分给 2 个人),同一条消息不会被同组两个人同时消费。
3.5 查看消费组和 LAG
powershell
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
查看某个组:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g1
输出类似:
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
g1 demo-topic 0 2 2 0 ...
g1 demo-topic 1 1 1 0 ...
g1 demo-topic 2 1 1 0 ...
三个关键列:
- CURRENT-OFFSET:这个组已提交到的位置;
- LOG-END-OFFSET:分区当前最新位置(下一条消息的编号);
- LAG :还差多少条没消费 =
LOG-END-OFFSET - CURRENT-OFFSET。
LAG 是 Kafka 最重要的监控指标。 LAG 持续上涨,说明消费速度跟不上生产速度。
想亲手制造一点 LAG:
- 停掉 g1 的所有消费者(Ctrl+C);
- 用 console producer 再发 5 条消息;
- 再执行
--describe --group g1,会看到 LAG 变成 5; - 重新启动 g1 消费者,消息消费完后 LAG 回到 0。
3.6 重置 offset(重放历史消息)
前提:这个消费组当前没有活跃消费者,否则会报错。
powershell
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic demo-topic --reset-offsets --to-earliest --execute
常用变体:
powershell
# 重置到最新(相当于跳过所有积压)
... --reset-offsets --to-latest --execute
# 重置到指定 offset
... --reset-offsets --to-offset 2 --execute
# 从当前位置往回退 3 条
... --reset-offsets --shift-by -3 --execute
# 重置到某个时间点之后
... --reset-offsets --to-datetime 2026-10-29T00:00:00.000 --execute
把 --execute 换成 --dry-run 可以先预览结果,不实际修改。
重置后再启动消费者,就会重新读历史消息,这就是「重放」。
3.7 查看分区最新 offset
powershell
docker exec -it kafka /opt/kafka/bin/kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic demo-topic
输出:
demo-topic:0:3
demo-topic:1:2
demo-topic:2:1
格式是 topic:partition:最新offset。可以拿它和消费组的 CURRENT-OFFSET 对照算 LAG。
3.8 修改 topic 配置
比如把保留时间改成 1 小时:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics --entity-name demo-topic --add-config retention.ms=3600000
查看当前覆盖的配置:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-configs.sh --bootstrap-server localhost:9092 --describe --entity-type topics --entity-name demo-topic
删除这个覆盖,恢复默认:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics --entity-name demo-topic --delete-config retention.ms
3.9 完整练习(建议按顺序做一遍)
powershell
# 1. 建一个 3 分区 topic
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic practice --partitions 3 --replication-factor 1
# 2. 发 6 条带 key 的消息(进入交互模式后逐行输入,Ctrl+C 退出)
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic practice --property parse.key=true --property key.separator=:
# user-1:下单
# user-2:下单
# user-1:支付
# user-3:下单
# user-2:支付
# user-1:发货
# 3. 用 g1 组从头消费,观察 key、partition、offset
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic practice --group g1 --from-beginning --property print.key=true --property print.partition=true --property print.offset=true
# 4. Ctrl+C 停掉,再发 3 条消息,然后看 LAG
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group g1
# 5. 重置到最早,重新消费
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic practice --reset-offsets --to-earliest --execute
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic practice --group g1 --from-beginning
做完这五步,你已经掌握了 Kafka 日常 80% 的命令行操作。
3.10 命令速查
powershell
# 建 topic
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic <name> --partitions 3 --replication-factor 1
# 列 topic
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
# 看详情
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic <name>
# 删 topic
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic <name>
# 发消息
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic <name>
# 收消息
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic <name> --from-beginning
# 看组
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group <group>
# 重置 offset
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group <group> --topic <name> --reset-offsets --to-earliest --execute
四、Spring Boot 实战:写一个能跑的生产者 + 消费者
目标:从零建一个 Spring Boot 项目,提供 HTTP 接口发消息,后台消费者把消息打印出来。下面给出完整代码,并解释每一行为什么这么写。
4.1 准备 JDK 17 和 Maven
Kafka 跑在 Docker 里,不需要本机 Java 。但 Spring Boot 4.x 需要 JDK 17 及以上。
先确认当前用的是哪个 JDK:
powershell
java -version
只对当前 PowerShell 窗口生效的切换方式(推荐,不影响系统里其他用旧 JDK 的项目):
powershell
$env:JAVA_HOME = "C:\Program Files\Java\jdk-17" # 换成你自己的 JDK 17 安装路径
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
java -version
mvn -v
mvn -v 里应该显示 Java version: 17.x。
在 IntelliJ IDEA 里则更简单:打开项目后 File → Project Structure → Project → SDK 选 17 即可,不用改系统环境变量。
不建议用
setx JAVA_HOME永久改成 17:你其他项目可能还在用 11,全局改容易出别的问题。
如果 Maven 下载依赖很慢,在 ~/.m2/settings.xml 的 <mirrors> 里加一个阿里云镜像:
xml
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
已经能正常下载依赖的话,这一段可以跳过。
4.2 版本要求
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 17+ | Spring Boot 4.x 最低要求 |
| Spring Boot | 4.1.2 | 当前稳定版 |
| spring-kafka | 4.1.2 | 由 Boot 管理,不用手写版本号 |
| kafka-clients | 4.2.2 | 由 Boot 管理 |
| Kafka broker | 4.4.0 | Docker 里的那个,客户端 4.2 连 4.4 完全兼容 |
Spring Boot 4 的变化 :Kafka 的 starter 改名为
spring-boot-starter-kafka(Boot 3 是直接引spring-kafka)。网上很多 Boot 3 的教程依赖写法不一样,注意区分。
4.3 项目结构
spring-kafka-demo/
├── pom.xml
└── src/main/
├── java/com/example/kafkademo/
│ ├── KafkaDemoApplication.java # 启动类
│ ├── config/TopicConfig.java # 启动时自动建 topic
│ ├── producer/MessageProducer.java
│ ├── consumer/MessageConsumer.java
│ └── web/MessageController.java
└── resources/application.yml
4.4 依赖:pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-kafka-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-kafka-demo</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
只引了两个 starter:
spring-boot-starter-kafka:带来 spring-kafka、kafka-clients 和自动配置;spring-boot-starter-web:提供 REST 接口方便测试。
4.5 配置:application.yml
yaml
server:
# 用 8081,避免和本机常见的 8080 服务冲突
port: 8081
spring:
application:
name: spring-kafka-demo
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: demo-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
ack-mode: batch
missing-topics-fatal: false
逐项解释:
bootstrap-servers:Kafka 地址。客户端只需要连上一个 broker,就能拿到整个集群的元数据。key-serializer/value-serializer:发送前把 Java 对象转成字节。这里用最简单的 String。group-id:消费组。同一个组内分区不重复分配。auto-offset-reset: earliest:组里没有已提交 offset 时,从最早开始读。注意:只在没有 offset 时生效。ack-mode: batch:Spring 的监听容器在一批消息处理完后提交 offset。这是 Spring 的默认行为,和原生客户端的enable.auto.commit不是一回事。missing-topics-fatal: false:topic 暂时不存在也不要让应用启动失败。
4.6 启动类
java
package com.example.kafkademo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.kafka.annotation.EnableKafka;
@EnableKafka
@SpringBootApplication
public class KafkaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(KafkaDemoApplication.class, args);
}
}
@EnableKafka 开启 @KafkaListener 注解的解析。Spring Boot 通常会自动帮你开启,但显式写上更清楚,也不会冲突。
4.7 启动时创建 topic
java
package com.example.kafkademo.config;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;
@Configuration
public class TopicConfig {
@Bean
public NewTopic demoTopic() {
return TopicBuilder.name("demo-topic")
.partitions(3)
.replicas(1)
.build();
}
}
Spring Boot 检测到 NewTopic 类型的 Bean 后,会通过 KafkaAdmin 在 broker 上创建它。已经存在则跳过。
单机环境副本数必须是 1,写 3 会创建失败。
生产环境更推荐用运维脚本或平台建 topic,把分区数、副本数、保留策略纳入版本管理;NewTopic Bean 适合本地开发和 demo。
4.8 生产者
java
package com.example.kafkademo.producer;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class MessageProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public MessageProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public CompletableFuture<SendResult<String, String>> send(String topic, String key, String message) {
if (key == null || key.isBlank()) {
// 没有 key:由 Kafka 的粘性分区器决定分区
return kafkaTemplate.send(topic, message);
}
// 有 key:hash(key) % 分区数,同一个 key 永远进同一个分区
return kafkaTemplate.send(topic, key, message);
}
}
KafkaTemplate 是 Spring 对 KafkaProducer 的封装,已经由 Boot 自动配置好,直接注入即可。
send 是异步 的,返回 CompletableFuture。生产环境可以加 .whenComplete(...) 记录成功/失败,或者用同步的 .get()(会阻塞,慎用)。
4.9 消费者
java
package com.example.kafkademo.consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
private static final Logger log = LoggerFactory.getLogger(MessageConsumer.class);
@KafkaListener(topics = "demo-topic", groupId = "demo-group")
public void consume(ConsumerRecord<String, String> record) {
log.info("收到消息 -> partition={}, offset={}, key={}, value={}",
record.partition(),
record.offset(),
record.key(),
record.value());
// 这里写你的业务逻辑:写数据库、调用下游、发通知......
}
}
@KafkaListener 背后是 Spring 启动的一个监听容器 :它在后台循环 poll 消息、反序列化、调用你的方法、按 ack-mode 提交 offset。你不需要自己写 while 循环。
方法参数可以直接是 String(只关心 value),也可以是 ConsumerRecord(需要 key/partition/offset 时)。这里用 ConsumerRecord 方便观察。
4.10 REST 接口
java
package com.example.kafkademo.web;
import com.example.kafkademo.producer.MessageProducer;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/messages")
public class MessageController {
private final MessageProducer producer;
public MessageController(MessageProducer producer) {
this.producer = producer;
}
@PostMapping
public Map<String, Object> send(@RequestParam String content,
@RequestParam(required = false) String key,
@RequestParam(defaultValue = "demo-topic") String topic) {
producer.send(topic, key, content);
return Map.of(
"status", "sent",
"topic", topic,
"key", key == null ? "" : key,
"content", content
);
}
}
4.11 运行和验证
第一步:确认 Kafka 在跑。
powershell
docker compose ps
第二步:切到 JDK 17 并启动应用。
powershell
$env:JAVA_HOME = "C:\Program Files\Java\jdk-17" # 换成你自己的 JDK 17 安装路径
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
cd spring-kafka-demo
mvn spring-boot:run
第一次会下载依赖,耐心等。看到下面的日志就启动成功了:
Tomcat started on port 8081
Started KafkaDemoApplication in x.xxx seconds
这里不用 8080,是为了避开本机常见的其他 Web 服务,端口冲突时改
application.yml即可。
第三步:另开一个终端发消息。
powershell
curl.exe -X POST "http://localhost:8081/messages?content=hello-kafka"
PowerShell 里
curl是Invoke-WebRequest的别名,所以这里写curl.exe。Linux / macOS 直接用curl。
返回:
json
{"status":"sent","topic":"demo-topic","key":"","content":"hello-kafka"}
回到应用控制台,应该能看到:
收到消息 -> partition=0, offset=0, key=null, value=hello-kafka
第四步:观察 key 和分区的关系。
powershell
curl.exe -X POST "http://localhost:8081/messages?content=order-created&key=order-1"
curl.exe -X POST "http://localhost:8081/messages?content=order-paid&key=order-1"
curl.exe -X POST "http://localhost:8081/messages?content=order-shipped&key=order-1"
三条消息的 key 都是 order-1,观察日志里的 partition,一定相同。这就是 Kafka 保证同一业务消息有序的方式。
第五步:用命令行独立验证。
用另一个消费组从头读一遍,证明消息确实在 Kafka 里:
powershell
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic demo-topic --group cli-check --from-beginning --property print.key=true --property print.partition=true
cli-check 是新的组,所以能从头读到所有消息,和 demo-group 互不影响。
4.12 背后发生了什么
- 自动配置 :
spring-boot-starter-kafka检测到配置,创建KafkaTemplate、ConsumerFactory、KafkaListenerContainerFactory、KafkaAdmin。 - 建 topic :
KafkaAdmin发现NewTopicBean,调用 AdminClient 创建demo-topic(3 分区)。 - 发消息 :Controller 收到请求 →
KafkaTemplate.send→ 序列化 → 选分区 → 攒批 → 发到 broker。 - 消费 :监听容器后台 poll → 反序列化 → 调
consume()→ 一批处理完提交 offset。 - offset 存在哪 :Kafka 内部的
__consumer_offsetstopic,所以重启应用后能从上次位置继续。
默认
ack-mode: batch意味着「先处理、后提交」。如果应用在处理完但提交前崩溃,重启后会重复消费,这就是 at-least-once。业务要按幂等设计(比如用订单号 + 状态机去重)。
4.13 进阶用法
手动提交 offset。
配置:
yaml
spring:
kafka:
listener:
ack-mode: manual_immediate
消费者方法加 Acknowledgment 参数,处理成功后手动确认:
java
import org.springframework.kafka.support.Acknowledgment;
@KafkaListener(topics = "demo-topic", groupId = "demo-group")
public void consume(ConsumerRecord<String, String> record, Acknowledgment ack) {
// 业务处理
log.info("处理: {}", record.value());
ack.acknowledge(); // 处理成功才提交
}
适合「宁可重复,不能丢」的场景。注意:如果业务处理抛异常且没 catch,消息不会提交,会触发重试。
发送 JSON。
Boot 4 / Spring Kafka 4 里,旧的 JsonSerializer 已标记为过时,新类是 JacksonJsonSerializer / JacksonJsonDeserializer。配置示例:
yaml
spring:
kafka:
producer:
value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer
consumer:
value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer
properties:
spring.json.trusted.packages: "com.example.kafkademo"
同时把消费者方法的参数改成你的 DTO 类型。Boot 4 需要 Jackson 3,确认 classpath 里有 spring-boot-starter-jackson(starter-web 一般会带)。
建议先用 String 发 JSON 字符串,把 Kafka 的机制跑通,再上自动序列化。少一层魔法,出错时更容易定位。
提高消费并发。
yaml
spring:
kafka:
listener:
concurrency: 3
每个并发对应一个消费者线程。并发数不要超过分区数,否则多出来的线程分不到分区。
指定分区发送。
java
kafkaTemplate.send(topic, partition, key, message);
一般不需要手动指定,交给 key 哈希更稳。
消费失败怎么办。
- 临时故障(下游超时):让异常抛出,Spring 的重试机制会重试;
- 永久故障(脏数据):重试多次后仍失败,应该进死信 topic(DLT),人工处理;
- Spring Kafka 提供
DefaultErrorHandler+DeadLetterPublishingRecoverer,可以搜索这两个类继续深入。
4.14 排错
| 现象 | 原因 / 解决 |
|---|---|
启动报 Class file has wrong version 61.0 |
运行或编译用的还是旧 JDK(61.0 对应 Java 17)。确认 mvn -v 显示 17,或 IDEA 的 Project SDK 改成 17 |
Connection to node -1 ... / 发送一直重试 |
Kafka 没启动,或 bootstrap-servers 地址不对 |
| 消费者收不到消息 | 检查 group-id、topic 名、auto-offset-reset;换个新 group 试 --from-beginning |
| 每次重启都重复消费 | 正常(at-least-once)。检查业务是否幂等,或改用 manual_immediate |
| 启动报 8081 端口占用 | 改 server.port,或停掉占用进程 |
启动很慢(约 40 秒),日志里有 Could not configure topics |
Kafka 没启动,KafkaAdmin 在建 topic 时超时。先 docker compose up -d 再启动应用 |
missing-topics-fatal 报错 |
已配置为 false;仍报错说明 Kafka 连不上,不是 topic 问题 |
IDEA 里 @KafkaListener 不生效 |
检查是否有 @EnableKafka、类是否被 Spring 扫描到(在启动类同包或子包下) |
五、优缺点与选型:什么时候该用 Kafka
学完怎么用,还要知道什么时候不该用。这一章帮你建立选型判断,面试也常考。
5.1 优点:Kafka 为什么流行
1. 吞吐量高。
单集群做到百万级消息/秒很常见。原因不是魔法,是几个工程优化叠加:
- 顺序写磁盘:只追加,不随机改,磁盘顺序写接近内存速度;
- 页缓存(page cache):读写都先走操作系统缓存,减少真实 IO;
- 零拷贝(zero-copy) :发送时用
sendfile,数据不从内核态拷到用户态; - 批量 + 压缩:生产者攒批、broker 批量落盘、消费者批量拉取,再配合 lz4/zstd 压缩;
- 分区并行:一个 topic 的吞吐可以随分区数水平扩展。
2. 持久化 + 可重放。
消息按策略保留(默认 7 天),消费者可以重置 offset 重新读。这让 Kafka 能当「事件存储」用:
- 新服务上线,可以从头消费历史数据回填;
- 线上出 bug,可以重放某段时间的消息复现;
- 流处理任务失败重启,从 checkpoint/offset 继续。
3. 多消费者、互不影响。
不同消费组各自维护 offset,同一条消息可以被风控、推荐、报表等多个系统各读一遍,生产者不用为每个下游单独发一份。
4. 水平扩展。
加 broker、加分区就能提升吞吐和存储。生产环境 3 broker 起步,几十上百 broker 的集群也很常见。
5. 生态完整。
- Kafka Connect:和各种数据源/目标对接(MySQL、ES、HDFS、S3),少写很多同步代码;
- Kafka Streams:库级别的流处理,不用额外部署计算集群;
- Schema Registry:管理 Avro/Protobuf schema 和兼容性;
- MirrorMaker 2:跨机房/跨云复制;
- 各大云厂商都有托管 Kafka。
6. 削峰填谷和解耦。
上游突发流量先写进 Kafka,下游按自己的能力消费,避免把数据库打挂。这是 Kafka 最经典的用法。
7. 顺序性可控。
按业务 key 分区,就能保证同一实体(订单、用户)的消息有序,不需要全局有序。
5.2 缺点:用之前必须知道的代价
1. 运维复杂度高。
Kafka 是一个分布式存储系统,不是一个轻量队列。你需要关心:
- 分区数规划、副本分布、磁盘容量;
- broker 扩容/缩容、版本升级;
- controller 选举、ISR 收缩、under-replicated 分区;
- 消费组 LAG、rebalance 频率;
- 监控、告警、日志清理。
KRaft 去掉了 ZooKeeper,部署简化了不少,但「分布式系统该有的复杂度」一个没少。小团队没有专职运维,建议直接用云托管。
2. 延迟不是它的强项。
为了吞吐,生产端会攒批(linger.ms 默认 5ms),端到端通常是毫秒级。如果你要的是微秒级 RPC、实时性极高的交易撮合,Kafka 不是最优解。
3. 存储成本。
消息不会因为消费而删除,保留期内是全量存储。流量大、保留久,磁盘成本很可观。虽然支持分层存储(tiered storage)把旧数据放对象存储,但会增加架构复杂度。
4. 功能相对「朴素」。
和 RabbitMQ 比,Kafka 缺少很多业务向的能力:
- 没有 Exchange 那种灵活路由(direct/topic/fanout/headers);
- 没有原生的单条消息延迟投递(需要自己用多级 topic + 定时任务实现);
- 没有优先级队列;
- 不支持「单条消息拒绝/重新入队」这种队列语义;
- 不适合请求-响应(RPC)模式。
5. 消费端要自己处理重复。
默认 at-least-once,网络抖动、rebalance、处理超时都可能导致重复。业务必须做幂等(唯一键、状态机、去重表)。
6. 分区数规划是门手艺。
- 分区太少:并行度不够、单分区成为瓶颈;
- 分区太多:元数据膨胀、文件句柄多、rebalance 慢、端到端延迟上升;
- 分区只能加不能减,加分区还会改变 key 的哈希映射,破坏顺序假设。
7. 不适合超大消息。
broker 默认单条消息上限约 1MB(message.max.bytes)。大文件、大图片不要塞 Kafka,应该存对象存储,消息里只放 URL。
8. 学习曲线。
分区、副本、ISR、offset、消费组、rebalance、事务......概念比 RabbitMQ 多一层。会用 不难,用好需要时间。
5.3 适合 / 不适合的场景
适合:
| 场景 | 说明 |
|---|---|
| 日志 / 埋点 / 监控数据采集 | 高吞吐、可多下游消费,Kafka 的主场 |
| 微服务事件驱动 | 订单创建、支付成功等事件广播给多个服务 |
| 削峰填谷 | 秒杀、大促流量先入 Kafka,后端匀速消费 |
| 数据管道 / CDC | 用 Debezium 抓 MySQL binlog,同步到 ES/数仓 |
| 流式计算 | 实时风控、实时推荐、指标聚合(配合 Flink/Streams) |
| 事件溯源 / 审计 | 保留完整事件历史,可回放 |
| 系统解耦 | 上游不关心下游有几个、什么时候消费 |
不适合:
| 场景 | 更适合的方案 |
|---|---|
| 任务队列(每任务只处理一次、要优先级/延迟) | RabbitMQ、Redis、云队列 |
| 请求-响应 / RPC | HTTP/gRPC |
| 超低延迟(微秒级) | 内存队列、RabbitMQ、专用交易系统 |
| 复杂路由(按 header/规则分发) | RabbitMQ |
| 单条消息延迟投递(如 30 分钟后提醒) | RabbitMQ 延迟插件、RocketMQ 延迟消息、定时任务 |
| 超大文件传输 | 对象存储 + 消息里放地址 |
| 只有一两个消费者、量很小的简单异步 | 数据库任务表、Redis、RabbitMQ 更省事 |
5.4 主流消息队列对比
下面的吞吐/延迟是量级,具体取决于硬件、配置和消息大小,不要当 benchmark 用。
| 维度 | Kafka | RabbitMQ | RocketMQ | Pulsar |
|---|---|---|---|---|
| 定位 | 分布式事件流平台 | 通用消息代理 | 阿里系业务消息 | 云原生消息/流平台 |
| 存储模型 | 分区日志 | 队列 | 队列(CommitLog) | 存算分离(BookKeeper) |
| 吞吐量级 | 百万/秒 | 万/秒 | 十万~百万/秒 | 百万/秒 |
| 延迟量级 | 毫秒 | 微秒~毫秒 | 毫秒 | 毫秒 |
| 顺序性 | 分区内 | 单队列内 | 队列内 | 分区内 |
| 延迟消息 | 无原生 | 插件 | 原生(5.x 任意时间) | 原生 |
| 事务消息 | 流处理事务 | 无 | 原生 | 事务 |
| 消息重放 | 原生 | Stream 类型支持 | 支持 | 支持 |
| 路由能力 | 弱 | 强 | 中 | 中 |
| 运维复杂度 | 高 | 低 | 中 | 高 |
| 典型用户 | 大数据/日志/流处理 | 业务解耦/任务队列 | 电商/金融 | 多租户/云原生 |
怎么选(简化版):
已经有 RabbitMQ,量不大、没痛点?
→ 别换,够用就好
日志采集、数据管道、流处理、需要重放、吞吐要求高?
→ Kafka
电商/金融,需要事务消息、延迟消息,团队在国内?
→ RocketMQ
多租户、存算分离、云原生环境、想要弹性扩缩?
→ Pulsar(但运维门槛更高)
团队小、没有专职中间件运维?
→ 优先云托管 Kafka(阿里云/腾讯云/Confluent Cloud),别自己搭生产集群
结论:Kafka 不是「更高级的 RabbitMQ」,两者解决的是不同问题。 很多公司两者并用:Kafka 做数据管道和事件流,RabbitMQ 做业务任务队列。
5.5 生产环境的最低配置认知
即使是入门,也要知道生产不是 docker compose up:
| 项目 | 建议 |
|---|---|
| broker 数量 | 至少 3 台,才能容忍坏 1 台 |
| 副本数 | 3 |
min.insync.replicas |
2 |
生产者 acks |
all |
| 磁盘 | 独立数据盘,容量按「日流量 × 保留天数 × 副本数 × 1.3」估算 |
| 监控 | LAG、under-replicated partitions、磁盘使用率、请求延迟、controller 状态 |
| 升级 | 滚动升级,先升 broker 再升客户端,注意协议兼容 |
| 安全 | SASL/SSL + ACL,别裸奔 |
单机 Docker 只适合学习和本地开发,不要直接搬上生产。
5.6 一页纸总结
Kafka 的优点:吞吐高、能存能重放、多消费者、可扩展、生态好。
Kafka 的代价:运维重、延迟毫秒级、存储贵、功能不花哨、消费端要幂等。
选型:
需要「高吞吐的事件管道 + 可重放的历史」选 Kafka;
需要「灵活路由 + 低延迟的任务队列」选 RabbitMQ;
别为了用 Kafka 而用 Kafka。
六、踩坑、面试与进阶
这一章当手册用,遇到问题先来这里查。
6.1 故障排查
连不上 Kafka。 按顺序检查:
powershell
# 1. 容器在不在
docker compose ps
# 2. 日志有没有启动成功
docker compose logs kafka | Select-String "Kafka Server started"
# 3. 端口通不通
Test-NetConnection localhost -Port 9092
# 4. 用 Kafka 自带脚本测协议连通性
docker exec -it kafka /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092
常见原因:
- 容器没起来 / 还在启动(等 10~30 秒);
bootstrap-servers写错(端口不是 9092、host 不是 localhost);- Docker 端口映射改了但客户端没跟着改;
- 远程访问时
advertised.listeners配的是localhost,远程客户端拿到 localhost 后连自己,必然失败。
消费者收不到消息。 依次排查:
- topic 名对不对(大小写敏感);
- 消费组是不是已经有 offset :换个新
group.id试--from-beginning; auto.offset.reset只在「没有已提交 offset」时生效,不是每次启动都从头读;- 消息是不是发到了别的 topic / 别的分区;
- 消费者有没有报反序列化异常(毒消息会导致一直重试或跳过);
- Spring 项目里
@KafkaListener所在类是否被扫描、是否加了@EnableKafka。
重复消费。 Kafka 默认 at-least-once,以下情况都会重复:
- 处理完、提交 offset 前进程崩溃或重启;
- rebalance 导致分区换人,新消费者从上次提交位置重读;
- 处理时间超过
max.poll.interval.ms,消费者被踢出组; - 生产者重试(Kafka 3.0+ 默认开启幂等,正常不会因此重复,除非跨会话或配置被改)。
解决思路:业务幂等,不要指望 Kafka 帮你做到 exactly-once。
常见幂等方案:
- 数据库唯一索引(业务 ID + 事件类型);
- 状态机(只允许 待支付→已支付,重复的「已支付」直接忽略);
- Redis
SETNX去重键,带过期时间; - 消费记录表(messageId 唯一)。
消息丢失。 可能原因:
| 原因 | 说明 |
|---|---|
acks=0 |
发出去就不管,broker 没收到也不知道 |
acks=1 |
leader 写完就确认,leader 挂了且未同步到 follower 就丢 |
min.insync.replicas=1 + acks=all |
ISR 只剩 leader 时仍算成功,随后 leader 挂就丢 |
| 生产者缓冲区满 | buffer.memory 满且 block.on.buffer.full=false(旧配置)时丢弃 |
| 自动提交 offset 早于处理 | 处理失败但 offset 已提交,重启后跳过 |
| 保留时间太短 | 消费者还没读,消息已被 retention 删除 |
防线:acks=all + replication.factor=3 + min.insync.replicas=2 + 关闭自动提交/处理成功后提交 + 合理保留时间 + 生产端处理发送失败。
顺序错乱。
- 没设 key:消息被分散到多个分区,分区之间本来就没有顺序;
- 同一 key 但分区数变了:哈希映射改变,历史和新消息可能不同分区;
- 生产端重试 + 多请求在途:老版本可能乱序,开启幂等(默认)后单分区内可保证;
- 消费端多线程处理同分区消息:自己把顺序打乱了。
保证顺序的做法:同一业务 key + 单分区消费 + 处理完再提交。
LAG 持续上涨。 说明消费速度 < 生产速度。处理顺序:
- 看是哪个分区、哪个消费者慢:
kafka-consumer-groups.sh --describe; - 消费者数是否已经等于分区数?没到就先加消费者;
- 单条处理是否太慢(DB 慢查询、远程调用)?优化业务或批量处理;
- 分区数是否成为瓶颈?评估后加分区(注意 key 顺序影响);
- 临时扩容消费能力,或先降级非核心业务。
Rebalance 频繁。 常见触发:
- 消费者处理太慢,超过
max.poll.interval.ms(默认 5 分钟); - 心跳超时(
session.timeout.ms,默认 45 秒); - 滚动发布频繁上下线;
- 消费者 GC 停顿过长。
缓解:
- 减小每次
max.poll.records,让单次处理更快返回; - 调大
max.poll.interval.ms(要匹配真实处理时间); - 用静态成员
group.instance.id,滚动重启时减少不必要的 rebalance; - 用 Cooperative Sticky Assignor(默认已较优)。
磁盘满。
powershell
# 看 Docker 整体占用
docker system df
# 如果用的是 1.6 节的 full 版 compose,数据目录是:
docker exec -it kafka du -sh /var/lib/kafka/data
最简版 compose 没有挂数据卷,数据目录在容器内的临时路径,看
docker system df就够了。
处理:
- 调小 topic 的
retention.ms/retention.bytes; - 清理无用 topic;
- 生产环境给 Kafka 独立数据盘,并监控磁盘水位;
- 不要手动删数据目录里的文件。
WSL / Docker 相关。
- Docker Desktop 占用内存大:Settings → Resources 限制 WSL 内存;
- WSL 磁盘只增不减:
wsl --manage <distro> --set-sparse true或定期 compact; - 时间不同步会导致 SSL/认证异常,重启 WSL 即可。
6.2 面试高频题(附答题要点)
1. Kafka 为什么快?
顺序写磁盘、页缓存、零拷贝(sendfile)、批量发送与压缩、分区并行。答题时别只说「因为快」,要说清每个机制解决什么问题。
2. Topic、Partition、Offset 的关系?
Topic 是逻辑分类,Partition 是物理分片(有序追加日志),Offset 是消息在分区内的唯一递增编号。分区是并行和扩展的单位。
3. 副本和 ISR 是什么?
每个分区有多个副本,一主多从。Leader 提供读写,Follower 同步。ISR 是与 leader 保持同步的副本集合。Leader 挂掉后从 ISR 中选新 leader。
4. acks=all 就一定不丢吗?
不一定。还要看 min.insync.replicas:如果 ISR 只剩 leader 且 min.insync.replicas=1,写入仍成功,leader 挂就丢。生产建议 RF=3、min.insync.replicas=2、acks=all,并禁用 unclean leader election(现代版本默认已禁用)。
5. 如何保证消息不丢?
生产端:acks=all + 重试 + 处理发送失败;broker:RF=3 + min.insync.replicas=2 + 合理刷盘;消费端:处理成功后再提交 offset;保留时间足够长。
6. 如何保证不重复消费?
Kafka 只能做到 at-least-once,重复不可避免。业务幂等:唯一索引、状态机、去重表、Redis SETNX。追求 exactly-once 时用 Kafka 事务 + read_committed,但只覆盖 Kafka 内部链路。
7. 如何保证顺序?
同一业务 key 进同一分区 + 单分区内单线程消费 + 处理完再提交。全局有序只能单分区,代价是吞吐。
8. 消费组和 rebalance?
同一消费组内一个分区只分给一个消费者,不同组独立。消费者上下线、心跳超时、分区变化会触发 rebalance,期间消费暂停。要减少 rebalance:控制处理时长、静态成员、合理超时。
9. Offset 存在哪?什么时候提交?
存在内部 topic __consumer_offsets。原生客户端默认每 5 秒自动提交;Spring Kafka 默认一批处理完后提交(ack-mode=batch)。提交的是「下一条要读的位置」。
10. 消费者是推还是拉?为什么?
拉(pull)。消费者能控制消费速率,避免被 broker 压垮;broker 不用维护每个消费者的状态;配合长轮询兼顾实时性。
11. 分区数怎么定?
按目标吞吐 / 单分区吞吐估算,再考虑消费者并行度上限。不是越多越好:分区多会增加元数据、文件句柄、rebalance 和端到端延迟。生产常见每 topic 6~48 个,创建时留一点余量。
12. Kafka 4.0 为什么移除 ZooKeeper?
KRaft 让 Kafka 自己管理元数据,架构更简单:少一个组件、部署和运维更轻、controller 故障恢复更快、支持更大规模集群。4.0 起 ZK 模式彻底删除。
13. 消息在磁盘上怎么存?
每个分区对应一个目录,内部按 segment 切分(默认 1GB),每个 segment 有 .log 数据文件和 .index / .timeindex 索引文件。按 offset 二分查找索引,再顺序读日志。
14. 零拷贝是什么?
传统发送要经过「磁盘 → 内核缓冲区 → 用户缓冲区 → socket 缓冲区 → 网卡」。零拷贝用 sendfile 让数据在内核态直接从页缓存送到网卡,减少两次拷贝和用户态/内核态切换。
15. 延迟队列怎么用 Kafka 实现?
Kafka 没有原生延迟消息。常见做法:按延迟级别建多个 topic(1s/5s/30s/1m...),消息先投到对应延迟 topic,由定时任务到点后转发到真实 topic;或者用时间轮 + 外部调度。复杂度高,简单场景直接用 RabbitMQ/RocketMQ 更合适。
16. 消息积压怎么处理?
先定位瓶颈(消费者数量、单条处理耗时、分区数),再:加到分区数个消费者 → 优化处理逻辑/批量写 → 临时扩容 → 极端情况下新建大分区 topic 做临时转发。事后补监控和容量规划。
6.3 把 Kafka 接进真实项目的检查清单
- 明确用途:是任务队列还是事件流?前者可能不该用 Kafka
- topic 命名规范:
环境.业务域.实体.事件,如prod.order.order.created - key 怎么选:要顺序就用业务主键;不要用随机值(会打散顺序)
- 分区数:按吞吐和消费者数估算,留余量
- 副本与 acks:生产至少 RF=3、min.insync.replicas=2、acks=all
- 消费幂等:唯一键/状态机/去重表
- 失败处理:重试策略 + 死信 topic + 告警
- 监控:LAG、消费速率、失败率、under-replicated 分区、磁盘水位
- 序列化:统一 JSON/Avro/Protobuf,带 schema 版本
- 安全:SASL/SSL、ACL,别裸奔
- 保留策略:按业务需要设置,别无脑 7 天
- 容量:日流量 × 保留天数 × 副本数 × 1.3
6.4 学习资源
官方(最权威,建议先看)
- Kafka 文档:https://kafka.apache.org/documentation/
- Quickstart:https://kafka.apache.org/quickstart
- Kafka 4.4 文档:https://kafka.apache.org/44/
- 官方 Docker 镜像说明:https://kafka.apache.org/44/getting-started/docker/
书
- 《Kafka 权威指南》(Kafka: The Definitive Guide)第 2 版。概念部分仍然经典;注意它写于 3.x 时代,4.x 已移除 ZooKeeper,看到 ZK 相关章节可以跳过。
- 《深入理解 Kafka:核心设计与实践原理》,偏源码和设计,适合进阶。
社区
- Confluent 开发者中心:https://developer.confluent.io/
- awesome-kafka 资源列表:https://github.com/conduktor/awesome-kafka
- Kafka Summit 的公开演讲(YouTube 可搜)
学习建议
- 视频只能帮你建立第一印象,一定要自己敲命令、看日志、改配置;
- 每学一个概念,就用 CLI 验证一次(比如「分区内有序」,就发带 key 的消息看分区);
- 遇到报错先看 broker 日志和客户端日志,Kafka 的报错信息通常很具体;
- 不要一上来啃源码,先把「用对」和「排错」练熟。
最后:
- Kafka 是分区日志,不是读完就删的队列。 offset 决定你读到哪,保留策略决定消息活多久。
- 并行靠分区,顺序靠 key,可靠靠副本 + acks,正确性靠业务幂等。
- 选型看场景:高吞吐、可重放、事件流选 Kafka;灵活路由、低延迟、任务队列选 RabbitMQ。
到这里,Kafka 的入门部分就完整了。剩下的就是动手:把容器跑起来,改改分区数,制造一次 LAG,重置一次 offset,再故意让消费者挂掉看看重复消费。