【Kafka】零基础学习教程

👋 关于作者

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

🏠 个人主页:一条泥憨鱼

通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!

如果你第一次接触 Kafka,很容易被 Topic、Partition、Offset、Consumer Group、ISR 这一串名词劝退。其实它的核心很简单:

Kafka 是一个只能追加、可以重复读的分布式日志。生产者往里写,消费者按自己的进度读,消息按策略保留,而不是读完就删。

先建立这个印象,后面的概念都是它的展开。

全文较长,建议先收藏,再按下方「怎么读」里的路线选择阅读方式。


写在前面

先看三句话

  1. Kafka 是一个「只能追加、可以重复读」的分布式日志,不是传统意义上「读完就删」的队列。
  2. 生产者写进去,消费者按自己的进度读。每个消费组自己记一个 offset(读到哪了),互不干扰。
  3. 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 窗口定义一个函数:

powershell 复制代码
function 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(分区)。每个分区是一个独立的、有序的、只能追加的日志文件。

分区带来三个好处:

  1. 水平扩展:不同分区可以放在不同 broker 上,吞吐随机器增加。
  2. 并行消费:一个消费组内,不同消费者可以同时消费不同分区。
  3. 局部有序:同一分区内严格有序。

代价也要知道:

  • 分区数不是越多越好。分区多了会增加 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)怎么工作

生产者发送一条消息,大致经历:

  1. 序列化:把对象变成字节(String、JSON、Avro...)。
  2. 选分区 :
    • 指定了 partition → 直接用;
    • 有 key → 对 key 做哈希,再对分区数取模;
    • 没有 key → 粘性分区(一段时间内集中写一个分区,攒批更高效)。
  3. 攒批 :消息先进入客户端缓冲区,凑够 batch.size 或等够 linger.ms 再一起发。
  4. 发送并等待确认 :按 acks 决定等谁确认。
  5. 失败重试 :按 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 → 空闲(没有分区可分)

由此得出两个重要结论:

  1. 消费者数量超过分区数,多出来的消费者会闲着。想提高消费并行度,先加分区。
  2. 同一个分区不会被同组两个消费者同时消费,所以组内不会重复消费(正常情况)。

拉模型(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 不保证全局顺序 ,只保证单个分区内的顺序。

想保证同一业务实体的消息有序:

  1. 用业务主键作为消息 key(如 orderId);
  2. Kafka 会对 key 哈希,同一个 key 永远进同一个分区;
  3. 该分区内按写入顺序消费。

注意:

  • 分区数一旦变化,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 五个常见误解

  1. 「Kafka 消息消费完就删了」 ❌

    错。按保留策略删除,和消费无关。这正是它能重放的原因。

  2. 「分区越多越好」 ❌

    分区有元数据、文件句柄、rebalance、端到端延迟等成本。够用就好。

  3. 「Kafka 能保证全局顺序」 ❌

    只保证单分区内有序。要业务有序就用 key。

  4. 「消费者加得越多消费越快」 ❌

    同组内并行度上限 = 分区数。消费者多于分区,多出来的空闲。

  5. 「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:

  1. 停掉 g1 的所有消费者(Ctrl+C);
  2. 用 console producer 再发 5 条消息;
  3. 再执行 --describe --group g1,会看到 LAG 变成 5;
  4. 重新启动 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 背后发生了什么

  1. 自动配置 :spring-boot-starter-kafka 检测到配置,创建 KafkaTemplate、ConsumerFactory、KafkaListenerContainerFactory、KafkaAdmin。
  2. 建 topic :KafkaAdmin 发现 NewTopic Bean,调用 AdminClient 创建 demo-topic(3 分区)。
  3. 发消息 :Controller 收到请求 → KafkaTemplate.send → 序列化 → 选分区 → 攒批 → 发到 broker。
  4. 消费 :监听容器后台 poll → 反序列化 → 调 consume() → 一批处理完提交 offset。
  5. offset 存在哪 :Kafka 内部的 __consumer_offsets topic,所以重启应用后能从上次位置继续。

默认 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 后连自己,必然失败。

消费者收不到消息。 依次排查:

  1. topic 名对不对(大小写敏感);
  2. 消费组是不是已经有 offset :换个新 group.id 试 --from-beginning;
  3. auto.offset.reset 只在「没有已提交 offset」时生效,不是每次启动都从头读;
  4. 消息是不是发到了别的 topic / 别的分区;
  5. 消费者有没有报反序列化异常(毒消息会导致一直重试或跳过);
  6. 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 持续上涨。 说明消费速度 < 生产速度。处理顺序:

  1. 看是哪个分区、哪个消费者慢:kafka-consumer-groups.sh --describe;
  2. 消费者数是否已经等于分区数?没到就先加消费者;
  3. 单条处理是否太慢(DB 慢查询、远程调用)?优化业务或批量处理;
  4. 分区数是否成为瓶颈?评估后加分区(注意 key 顺序影响);
  5. 临时扩容消费能力,或先降级非核心业务。

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 权威指南》(Kafka: The Definitive Guide)第 2 版。概念部分仍然经典;注意它写于 3.x 时代,4.x 已移除 ZooKeeper,看到 ZK 相关章节可以跳过。
  • 《深入理解 Kafka:核心设计与实践原理》,偏源码和设计,适合进阶。

社区

学习建议

  1. 视频只能帮你建立第一印象,一定要自己敲命令、看日志、改配置;
  2. 每学一个概念,就用 CLI 验证一次(比如「分区内有序」,就发带 key 的消息看分区);
  3. 遇到报错先看 broker 日志和客户端日志,Kafka 的报错信息通常很具体;
  4. 不要一上来啃源码,先把「用对」和「排错」练熟。

最后:

  1. Kafka 是分区日志,不是读完就删的队列。 offset 决定你读到哪,保留策略决定消息活多久。
  2. 并行靠分区,顺序靠 key,可靠靠副本 + acks,正确性靠业务幂等。
  3. 选型看场景:高吞吐、可重放、事件流选 Kafka;灵活路由、低延迟、任务队列选 RabbitMQ。

到这里,Kafka 的入门部分就完整了。剩下的就是动手:把容器跑起来,改改分区数,制造一次 LAG,重置一次 offset,再故意让消费者挂掉看看重复消费。

相关推荐
Wang's Blog1 小时前
Java 项目实战:交友项目 - 即时通信总结:全链路复盘、可复用清单与自检
java·python·交友
文丑颜不良啊1 小时前
LeetCode1501~1520题目解析
java·leetcode
CAIE研习社2 小时前
30岁以后学习AI应用,证书能起到什么作用?
人工智能·学习
长不大的杨瑞杰2 小时前
OpenCode v2 LSP 实战:内置语言服务器、自定义配置与诊断闭环
java·人工智能·typescript·lsp·opencode
云和数据.ChenGuang2 小时前
python和java的try
java·开发语言·python
叼烟扛炮2 小时前
Linux 学习笔记(四十五):Web 安全基石——Cookie、Session 与 HTTPS 原理解析
linux·前端·网络·笔记·学习·tcp/ip·安全
日日大王学编程2 小时前
阿里巴巴开源 Data Agent(Text2SQL)项目全维度详解
java·ai·开源
Wang's Blog2 小时前
Java 项目实战:交友项目 - 交友左滑右滑
java·交友
zhangrelay2 小时前
Arduino-MEGA-ESP单片机理论课笔记-01入门-02硬件-03开发环境-2026
笔记·单片机·嵌入式硬件·学习·机器人