CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Kafka 消息队列入门/课时/生产者
RawBlame
课时目录12 节

Kafka 消息队列入门

  1. 01消息队列是什么
  2. 02核心概念
  3. 03Docker 运行 Kafka
  4. 04生产者
  5. 05消费者组
  6. 06Spring Kafka
  7. 07消息格式
  8. 08投递语义
  9. 09Kafka Streams 入门
  10. 10监控与 lag
  11. 11Kafka vs RabbitMQ
  12. 12综合练习

生产者

发送消息、key 分区、acks 语义。

35 分钟

生产者

这一课你将能做到

  1. 用 Java KafkaProducer 向 coderoadmap.order-events 发送带 orderId key 的 JSON,并在回调里打印 partition/offset
  2. 对照表格说清 acks=0/1/all 各自「可能丢什么、换来什么延迟」,并能据此为订单事件选型
  3. 说明「先 MySQL 事务提交再 send」的原因,并指出无 key 导致乱序的错法

为什么要学

Consumer、Spring Kafka、投递语义课都建立在「消息已经可靠进了分区日志」这个前提上。Producer 侧三件套:序列化、分区键、acks/重试。配错会出现两种事故:以为发出去了其实丢了(下单成功通知永远不来)、同订单事件乱序(Shipped 比 Paid 先被消费)。

本课的代码独立成类即可运行,与你的 Spring Boot 主工程(MySQL + Redis)互不干扰;心智上请记住:Producer 只负责把事件可靠写入日志,业务成败仍由数据库事务决定。

人话:Producer = 往日记本某一页(分区)追加一行,并选择「等多久才敢说这句写稳了」。

先建立图像

业务线程
  │ 1. MySQL 提交订单(真相)
  │ 2. kafkaProducer.send(record)
  ▼
key/value 序列化 → 按 key 哈希选 partition → 进 RecordAccumulator 批量缓冲
  │
  ├─ acks=0   : 不等 Broker 确认(最快,可能丢)
  ├─ acks=1   : Leader 分区写入即确认(折中)
  └─ acks=all : ISR 内所有副本确认(最稳,稍慢)
       │
       ▼ 回调 (metadata | exception)
acks谁确认才算成功丢消息风险典型场景
0没人,发出即算网络/Broker 一挂就丢可丢的指标、埋点
1Leader 写入Leader 确认后立刻宕机且未同步给 Follower 时丢一般业务事件
all(-1)ISR 全部副本配合 min.insync.replicas≥2 基本不丢订单、支付相关事件

跟着做

步骤 1:确认 Broker 与 Topic

按上一课保证 localhost:9092 可用,Topic coderoadmap.order-events 3 分区。核对命令:

docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic coderoadmap.order-events

步骤 2:写最小可运行 Producer

Maven 依赖(Boot 项目加 spring-kafka 即含客户端;纯 Java 项目用 org.apache.kafka:kafka-clients:3.9.0):

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.9.0</version>
</dependency>

完整代码:

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class OrderEventProducerDemo {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 订单类事件:等 ISR 全部确认
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        // 幂等生产者:Broker 端按 (PID, 分区, 序号) 去重,网络重试不重复追加
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
        props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
        // 批量与延迟微调:攒 16KB 或 10ms 再发,吞吐更高
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

        try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
            String orderId = "1001"; // key:决定分区,保同订单顺序
            String json = "{\"event\":\"OrderCreated\",\"orderId\":\"1001\"}";
            ProducerRecord<String, String> record =
                    new ProducerRecord<>("coderoadmap.order-events", orderId, json);

            producer.send(record, (metadata, exception) -> {
                if (exception != null) {
                    // 失败必须当一等公民处理:记日志/告警/补偿,不能假装成功
                    System.err.println("发送失败: " + exception.getMessage());
                } else {
                    System.out.printf("成功 partition=%d offset=%d%n",
                            metadata.partition(), metadata.offset());
                }
            });
            producer.flush(); // 进程退出前把缓冲刷出
        }
    }
}

运行后应看到 partition=x offset=n;再用 console consumer --from-beginning 核对 JSON 已在 Topic 里。

步骤 3:对比 acks 的行为

把 ACKS_CONFIG 分别改成 1 与 all,各发 10 条,观察回调全部成功、主观延迟差异不大(本地实验)。然后停掉 Kafka(docker compose stop kafka)再 send:回调里应出现异常。笔记写结论:业务层必须处理回调异常,fire-and-forget 不存在。

步骤 4:验证 key 与分区

连续发同一 orderId 的 Created、Paid 两条,看回调 partition 是否相同;再发几条 key=null 的,观察 partition 轮换。应看到:稳定 key → 同分区;无 key → 分散,失去订单级顺序。

步骤 5:接到「事务后发送」心智

对照 Spring Boot 事务课:

@Transactional
public void createOrder(CreateOrderCmd cmd) {
    orderRepo.save(Order.from(cmd)); // 先落库,真相在 MySQL
    // 事务提交成功后再发;事务内先发、回滚后会下游处理"幽灵订单"
    // 更稳的方案(Outbox)见"投递语义"课
}

写一句进笔记:先 send 再 DB 回滚 = 下游处理一笔不存在的订单。

步骤 6(可选):KafkaTemplate 一眼

Spring 项目里同样的配置收敛到 yml,发送一行:

kafkaTemplate.send("coderoadmap.order-events", orderId, json);

细节留给「Spring Kafka」课展开。

讲清楚要点

  1. 因为Broker 只存字节,所以必须配 key/value Serializer;实验用 StringSerializer,POJO 走 JSON(消息格式课)。
  2. 因为默认分区器对 key 哈希(murmur2),所以同实体用同一 key 才能分区内有序;key 一变,顺序承诺作废。
  3. 因为acks=all 等 ISR 全部确认,所以Leader 宕机也不会丢已确认消息,代价是延迟略高;订单/支付类事件值得。
  4. 因为send 是异步的(写进本地缓冲就返回 Future),所以失败只能靠回调或 get() 感知;进程退出前要 flush()/close()。
  5. 因为网络重试可能造成 Broker 重复追加,所以开 enable.idempotence 让 Broker 按 (PID, partition, seq) 去重——它只管生产侧,不管消费侧重复。
  6. 因为跨分区没有全局顺序,所以不要用「整个 Topic 单分区」硬保全局序扛吞吐;按实体 key + 分区序才是标准解。

你可能会懵的地方

  1. 「回调成功 = 下游已处理?」 回调成功只代表写入日志。下游消费、业务处理、幂等是另外三层,互不包含。

  2. 「acks=all 是不是 exactly-once?」 不是。acks=all + 幂等生产者只解决「可靠写入且 Broker 不重复追加」;端到端 exactly-once 还涉及消费提交与业务幂等(投递语义课)。别在简历上一句话画等号。

  3. 「同步 get() 是不是更安全?」 关键路径短超时 get(1, SECONDS) 可接受;高吞吐场景批量异步 + 监控错误率,别让业务线程堵在 Future 上拖垮 Tomcat 线程池。

  4. 「key 选 slug 还是 lessonId?」 选生命周期内稳定且需要保序的实体 id。slug 可能被改,单独做顺序键会翻车;lessonId/orderId 稳定。

  5. 「retries 拉满会不会无限重试?」 配合 delivery.timeout.ms(默认 120s)封顶:单条消息从发送到确认的总时限,超时回调报错。重试交给客户端,时限你来定。

错法对照

错法问题正解
所有事件 key=null 却要求同订单顺序消息轮询散落多分区,消费乱序key=orderId 稳定路由
acks=0 发支付结果还当成功进程/网络抖动即丢,无法追责关键事件 acks=all + 回调处理
DB 未提交就 send,回滚后下游照常消费下游处理「幽灵订单」事务提交后 send,或上 Outbox
忽略回调异常,只打一行 log「偶尔丢消息」无从排查失败进重试队列/死信并告警
用单分区 Topic 换全局有序吞吐锁死在 1,积压风险高业务实体 key + 接受分区级顺序

卡点急救

  1. 发送报 TimeoutException: Expiring 1 record(s):Broker 连不上或缓冲积压超时。先确认容器 Up、localhost:9092 可达,再查 advertised.listeners。
  2. 回调一次都不触发:主线程先退出、缓冲没刷出去。flush() 或让 producer 生命周期覆盖整个发送过程(try-with-resources)。
  3. 同订单消息还是乱序:查三点——key 是否真的相同、中间是否有 partitioner.class 自定义、消费端是否多线程乱处理(按 key 再分发到单线程队列)。

面试常问

  • Kafka 如何保证消息不丢(生产者角度)? 三层配置:acks=all 等 ISR 全部确认;retries + delivery.timeout.ms 保证网络抖动自动重试;enable.idempotence=true 保证重试不重复写入。再加服务端 min.insync.replicas>=2 与 unclean.leader.election.enable=false,避免「ISR 只剩 1 个也放行」和「落后副本当 Leader」。

  • ISR 是什么?为什么 acks=all 要依赖它? ISR(In-Sync Replicas):与 Leader 保持同步的副本集合(含 Leader 自己)。Follower 落后超过 replica.lag.time.max.ms 会被踢出。acks=all 的含义是「ISR 内所有副本都写入才算成功」——所以 ISR 缩到 1 时 all 退化成 1;用 min.insync.replicas 兜底,低于下限直接拒绝写入,宁可不可用也不丢数据。

  • 怎么保证同一订单的消息有序? Producer 用 orderId 作 key(哈希固定分区)+ 分区内追加有序 + 消费端同 key 串行处理(或多线程内按 key 哈希到固定线程)。补充代价:大订单量的 key 会造成分区热点,可评估组合 key。

自测三问

  1. acks=0/1/all 各用一句话说明「谁确认才返回」;订单事件你选哪个?为什么?
  2. 故意停掉 Kafka 再 send,回调应出现什么?业务层至少做哪两件事?
  3. 课程 slug 变更事件:key 选 slug 还是稳定 lessonId?各写一句利弊。

小结一句话

生产者负责可靠写入与合理分区:稳定 key 保序、acks/重试/幂等保不丢,回调异常必须处理;业务真相仍在 MySQL。

延伸

  • Kafka 官方文档 — Producer Configs
  • 下一课「消费者组」:另一端如何拉取、提交位点、参与 rebalance。
加载本节练习…
← 上一课Docker 运行 Kafka下一课 →消费者组