👋 关于作者
你好,这里是一条泥憨鱼 | 专注于 后端开发、AI 前沿技术与计算机基础 。
🏠 个人主页:一条泥憨鱼
通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!
如果你第一次接触 Kafka,很容易被 Topic、Partition、Offset、Consumer Group、ISR 这一串名词劝退。其实它的核心很简单:
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 正常。
wsl —status
wsl —update
如果提示需要重启,就重启一次。
第二步:安装 Docker Desktop。
winget install –e —id Docker.DockerDesktop
安装完成后注销并重新登录 Windows(或者重启),让 docker 命令进入 PATH。
想把 Docker 装到非系统盘,可以改用官方安装器参数:
"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 里加上:
"cliPluginsExtraDirs": ["D:\\\\Docker\\\\resources\\\\cli-plugins"]
第三步:启动并检查。
从开始菜单打开 Docker Desktop:首次启动接受协议,可以跳过登录,左下角显示 Engine running 才算启动完成。
新开一个 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:
# 最简单机 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
在该目录下执行:
docker compose up –d
第一次会下载 apache/kafka:4.4.0 镜像,耐心等。看到类似输出就成功了:
[+] Running 2/2
✔ Network demo_default Created
✔ Container kafka Started
检查容器状态:
docker compose ps
STATUS 应该是 Up …。再看日志确认 Kafka 真的启动了:
docker compose logs kafka
在输出里找这一行:
Kafka Server started
单机模式下,同一个进程既是 broker 又是 controller(KRaft 的 combined 模式),所以只有一个容器、没有 ZooKeeper。官方镜像自带一套「单机 KRaft 可用」的默认配置,因此只要映射 9092 端口就能用。学习阶段这样就够了。
1.4 验证:第一条 Kafka 命令
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。
再确认一下版本:
docker exec –it kafka /opt/kafka/bin/kafka-topics.sh —version
应该显示 4.4.0。
容器里的 Kafka 命令都在 /opt/kafka/bin/ 下,所以每条命令都要带这个前缀。嫌长可以在当前 PowerShell 窗口定义一个函数:
function kfk-topics { docker exec –it kafka /opt/kafka/bin/kafka-topics.sh @args }
之后就可以写 kfk-topics –bootstrap-server localhost:9092 –list。
1.5 常用运维命令
# 启动(后台)
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。它把关键参数都写出来了,更适合理解:
# 显式配置版:把关键参数写清楚,并挂载数据卷做持久化
# 启动: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 的元数据、文件句柄、选举和 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_offsets topic 里。
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)怎么工作
生产者发送一条消息,大致经历:
- 指定了 partition → 直接用;
- 有 key → 对 key 做哈希,再对分区数取模;
- 没有 key → 粘性分区(一段时间内集中写一个分区,攒批更高效)。
常用参数(初学先知道有这些):
| 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 到分区的映射会变,历史消息和新消息可能不在同一分区,顺序假设会被打破。
- 全局有序只能把 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。每条命令的完整形式是:
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:
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:
docker exec –it kafka /opt/kafka/bin/kafka-topics.sh —bootstrap-server localhost:9092 —list
查看某个 topic 的详情:
docker exec –it kafka /opt/kafka/bin/kafka-topics.sh —bootstrap-server localhost:9092 —describe —topic demo-topic
输出类似:
Topic: demo-topicTopicId: xxxxPartitionCount: 3ReplicationFactor: 1Configs:
Topic: demo-topicPartition: 0Leader: 1Replicas: 1Isr: 1Elr: LastKnownElr:
Topic: demo-topicPartition: 1Leader: 1Replicas: 1Isr: 1Elr: LastKnownElr:
Topic: demo-topicPartition: 2Leader: 1Replicas: 1Isr: 1Elr: LastKnownElr:
- Leader:负责这个分区的 broker id(单机就是 1);
- Replicas:副本所在 broker;
- Isr:当前同步的副本。
增加分区(只能加,不能减):
docker exec –it kafka /opt/kafka/bin/kafka-topics.sh —bootstrap-server localhost:9092 —alter —topic demo-topic —partitions 6
生产环境慎用:分区数变化会影响按 key 的哈希映射,可能破坏「同一 key 顺序消费」的假设。
删除 topic:
docker exec –it kafka /opt/kafka/bin/kafka-topics.sh —bootstrap-server localhost:9092 —delete —topic demo-topic
3.3 发消息:console producer
打开一个终端执行:
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
从最早的消息开始读。 新开一个终端:
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):
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 继续读。
再开一个不同组的消费者:
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:
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
docker exec –it kafka /opt/kafka/bin/kafka-consumer-groups.sh —bootstrap-server localhost:9092 —list
查看某个组:
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:
3.6 重置 offset(重放历史消息)
前提:这个消费组当前没有活跃消费者,否则会报错。
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
常用变体:
# 重置到最新(相当于跳过所有积压)
... —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
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 小时:
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
查看当前覆盖的配置:
docker exec –it kafka /opt/kafka/bin/kafka-configs.sh —bootstrap-server localhost:9092 —describe —entity-type topics —entity-name demo-topic
删除这个覆盖,恢复默认:
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 完整练习(建议按顺序做一遍)
# 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 命令速查
# 建 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:
java –version
只对当前 PowerShell 窗口生效的切换方式(推荐,不影响系统里其他用旧 JDK 的项目):
$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> 里加一个阿里云镜像:
<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 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
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 启动类
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
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 生产者
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 消费者
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 接口
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 在跑。
docker compose ps
第二步:切到 JDK 17 并启动应用。
$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 即可。
第三步:另开一个终端发消息。
curl.exe –X POST "http://localhost:8081/messages?content=hello-kafka"
PowerShell 里 curl 是 Invoke-WebRequest 的别名,所以这里写 curl.exe。Linux / macOS 直接用 curl。
返回:
{"status":"sent","topic":"demo-topic","key":"","content":"hello-kafka"}
回到应用控制台,应该能看到:
收到消息 -> partition=0, offset=0, key=null, value=hello-kafka
第四步:观察 key 和分区的关系。
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 里:
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 背后发生了什么
默认 ack-mode: batch 意味着「先处理、后提交」。如果应用在处理完但提交前崩溃,重启后会重复消费,这就是 at-least-once。业务要按幂等设计(比如用订单号 + 状态机去重)。
4.13 进阶用法
手动提交 offset。
配置:
spring:
kafka:
listener:
ack-mode: manual_immediate
消费者方法加 Acknowledgment 参数,处理成功后手动确认:
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。配置示例:
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 的机制跑通,再上自动序列化。少一层魔法,出错时更容易定位。
提高消费并发。
spring:
kafka:
listener:
concurrency: 3
每个并发对应一个消费者线程。并发数不要超过分区数,否则多出来的线程分不到分区。
指定分区发送。
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 用。
| 定位 | 分布式事件流平台 | 通用消息代理 | 阿里系业务消息 | 云原生消息/流平台 |
| 存储模型 | 分区日志 | 队列 | 队列(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。 按顺序检查:
# 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 后连自己,必然失败。
消费者收不到消息。 依次排查:
重复消费。 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 持续上涨。 说明消费速度 < 生产速度。处理顺序:
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(默认已较优)。
磁盘满。
# 看 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 可搜)
学习建议
最后:
到这里,Kafka 的入门部分就完整了。剩下的就是动手:把容器跑起来,改改分区数,制造一次 LAG,重置一次 offset,再故意让消费者挂掉看看重复消费。



