初识 Kafka

1. 什么是 Kafka

Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。


2. Kafka 的基本术语

术语 说明
消息(Message) Kafka 中的数据单元被称为消息,也被称为记录,可以把它看作数据库表中某一行的记录。
批次(Batch) 为了提高效率,消息会分批次写入 Kafka,批次就代指的是一组消息。
主题(Topic) 消息的种类称为主题,可以说一个主题代表了一类消息,相当于是对消息进行分类。主题就像是数据库中的表。
分区(Partition) 主题可以被分为若干个分区,同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 Kafka 的伸缩性。单一主题中的分区有序,但是无法保证主题中所有的分区有序。
生产者(Producer) 向主题发布消息的客户端应用程序称为生产者,生产者用于持续不断地向某个主题发送消息。
消费者(Consumer) 订阅主题消息的客户端程序称为消费者,消费者用于处理生产者产生的消息。
消费者群组(Consumer Group) 生产者与消费者的关系就如同餐厅中的厨师和顾客之间的关系一样,一个厨师对应多个顾客,也就是一个生产者对应多个消费者。消费者群组指的是由一个或多个消费者组成的群体。
偏移量(Offset) 偏移量是一种元数据,它是一个不断递增的整数值,用来记录消费者发生重平衡时的位置,以便用来恢复数据。
Broker 一个独立的 Kafka 服务器就被称为 Broker,Broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。
Broker 集群 Broker 是集群的组成部分,Broker 集群由一个或多个 Broker 组成,每个集群都有一个 Broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。
副本(Replica) Kafka 中消息的备份又叫做副本,副本的数量是可以配置的。Kafka 定义了两类副本:领导者副本(Leader Replica)追随者副本(Follower Replica),前者对外提供服务,后者只是被动跟随。
重平衡(Rebalance) 消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。
在这里插入图片描述
在这里插入图片描述

3. Kafka 的特性(设计原则)

  1. 高吞吐、低延迟:Kafka 最大的特点就是收发消息非常快,Kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。
  2. 高伸缩性:每个主题(Topic)包含多个分区(Partition),主题中的分区可以分布在不同的主机(Broker)中。
  3. 持久性、可靠性:Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失。Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 的数据能够持久存储。
  4. 容错性:允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作。
  5. 高并发:支持数千个客户端同时读写。

4. Kafka 的使用场景

  1. 活动跟踪:Kafka 可以用来跟踪用户行为,比如打开淘宝的那一刻,登录信息、登录次数都会作为消息传输到 Kafka;浏览购物时,浏览信息、搜索指数、购物爱好都会作为一个个消息传递给 Kafka,这样就可以生成报告,做智能推荐,购买喜好等。
  2. 传递消息:Kafka 另外一个基本用途是传递消息,应用程序向用户发送通知就是通过传递消息来实现的,这些应用组件可以生成消息,而不需要关心消息的格式,也不需要关心消息是如何发送的。
  3. 度量指标:Kafka 也经常用来记录运营监控数据,包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
  4. 日志记录:Kafka 的基本概念来源于提交日志,比如可以把数据库的更新发送到 Kafka 上,用来记录数据库的更新时间,通过 Kafka 以统一接口服务的方式开放给各种 Consumer,例如 Hadoop、HBase、Solr 等。
  5. 流式处理:流式处理是一个能够提供多种应用程序的领域。
  6. 限流削峰:Kafka 多用于互联网领域某一时刻请求特别多的情况下,可以把请求写入 Kafka 中,避免直接请求后端程序导致服务崩溃。

Kafka 的消息队列

5. 消息队列的两种模式

Kafka 的消息队列一般分为两种模式:点对点模式发布订阅模式

Kafka 是支持消费者群组的,也就是说 Kafka 中会有一个或者多个消费者。如果一个生产者生产的消息由一个消费者进行消费的话,那么这种模式就是点对点模式;如果一个生产者或者多个生产者产生的消息能够被多个消费者同时消费,这样的消息队列称为发布订阅模式

5.1 点对点模式(Point-to-Point)

关键点:

  • 行为:在同一个消费者群组内部,一个分区最多只能被组内的一个消费者实例消费。
  • 目的:负载均衡(Load Balancing)。通过增加消费者实例,可以更快地处理完 Topic 里的所有数据,这就是所谓的横向伸缩。
    在这里插入图片描述

5.2 发布订阅模式(Pub-Sub)

关键点:

  • 行为:不同的消费者群组,订阅同一个主题,它们之间是完全独立的。每一个群组都会收到这个主题的全量消息。
  • 目的:数据共享与广播(Data Sharing / Broadcast)。这使得同一份数据源可以被多个不同的系统同时使用。例如:
    • 订单系统(群组 A)消费它来处理订单
    • 数据分析系统(群组 B)消费它来进行实时统计
    • 风控系统(群组 C)消费它来检测异常交易
      在这里插入图片描述

5.3 为什么如此高效?

Kafka 之所以能高效地支持大量群组同时消费,是因为它在设计上是**“无状态”**的。当一个群组读取一条消息时,Kafka 并不会删除这条数据。它只是在这个群组自己的"订阅卡"上,更新一下偏移量(Offset),也就是记录下该群组已经读到哪个位置了。而其他群组则有自己独立的、互不干扰的偏移量记录。

因此,增加再多的消费者群组,对于 Kafka 来说,只是多维护几张独立的"订阅卡"而已,并不会对核心的数据存储和读取造成大的性能影响。


Kafka 架构概述

6. Kafka 集群架构

一个典型的 Kafka 集群中包含若干 Producer(可以是 Web 前端产生的 Page View,或者是服务器日志、系统 CPU、Memory 等),若干 Broker(Kafka 支持水平扩展,一般 Broker 数量越多,集群吞吐率越高),若干 Consumer Group,以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 Leader,以及在 Consumer Group 发生变化时进行 Rebalance。Producer 使用 push 模式将消息发布到 Broker,Consumer 使用 pull 模式从 Broker 订阅并消费消息。


7. 核心 API

7.1 Producer API(生产者 API)

  • 功能:把数据"寄"到 Kafka 仓库里。
  • 示例:当在前端点击"上传 PDF",Spring Boot 后端会生成一个任务信息(比如文件路径、用户 ID),调用 Producer API 把这个信息像发快递一样塞进 Kafka 的 file-processing 主题里。
  • 一句话理解:只管往里塞,不管谁来拿。

7.2 Consumer API(消费者 API)

  • 功能:从 Kafka 仓库里把数据"取"出来处理。
  • 示例FileProcessingConsumer 这个类就在使用 Consumer API,它一直盯着仓库看,一旦发现有新快递(PDF 任务),就把它拿出来,拆开,然后调 AI 接口解析。
  • 一句话理解:盯着仓库看,有货我就拿。

7.3 Streams API(流处理 API)

  • 功能:它是 Producer 和 Consumer 的结合体。它从一个主题拿数据,实时加工一下,立刻塞进另一个主题。
  • 示例:从"原始消息"主题拿走一句英文 → 实时翻译成中文 → 立刻塞进"中文消息"主题。
  • 用途:做实时计算,比如实时统计全站 1 分钟内有多少人提问,不需要写复杂的逻辑,用 Streams API 就像写 SQL 一样简单。
  • 一句话理解:边收、边改、边发(生产线)。

7.4 Connector API(连接器 API)

  • 功能:为了"省事"。如果想把别的地方(比如 MySQL 或 ES)的数据搬进 Kafka,不需要自己写代码,直接用现成的连接器。
  • 示例:让 MySQL 里的 users 表一有新用户,Kafka 就能收到信。
    • 不用 Connector:得在 Java 业务代码里写 userMapper.insert(user); kafkaTemplate.send(user);
    • 用 Connector:装一个"MySQL 连接器",它会盯着 MySQL 的日志,只要数据库变了,它自动把变化同步到 Kafka,一行 Java 代码都不用写。
  • 一句话理解:打通两个仓库的自动搬运机。

8. 提交日志(Commit Log)

8.1 什么是提交日志?

想象一下法庭上的书记员或者轮船上的航海日志,它们有以下特点:

  • 只能追加,不能修改(Append-only & Immutable):书记员只会把新说的话写在记录本的最后面,绝不会翻到前面去修改已经记录下来的证词。一旦写下,就成了永久的、不可更改的记录。
  • 严格有序(Strictly Ordered):记录是严格按照时间顺序一字不差地记下来的。第 100 句证词一定是在第 99 句之后说的。
  • 持久的事件记录(Durable Record of Events):它的唯一目的,就是忠实地记录下发生过的每一件事(每一次"提交")。

"提交日志"就是一个严格按照时间顺序、只能追加、不可更改的事件流水账。它的名字就体现了它的特性:一旦一个事件被记录(“提交”),它就成为了一个既定事实,被永久地记在了日志(Log)里。

8.2 一个分区就是一个提交日志

把上面的三个特点套在 Kafka 的一个分区(Partition)上,会发现它们是完美对应的:

只能追加,不能修改:

  • 生产者(Producer)发送消息到某个分区时,这个消息总是被追加到该分区日志文件的末尾。
  • 永远不能修改或删除分区中间的某条消息。数据一旦写入,就是不可变的(Immutable)。

严格有序:

  • 分区内的每一条消息都会被分配一个唯一的、从 0 开始递增的序号,这个序号叫做偏移量(Offset)。
  • 比如,一个分区里有三条消息,它们的 Offset 依次是 0, 1, 2。消费者来读取时,也是按照这个顺序来读取,从而保证了在这个分区内部,消息的处理顺序和写入顺序完全一致。

持久的事件记录:

  • 分区本质上就是 Broker 服务器上的一个或多个日志文件。消息被持久化存储在磁盘上,直到超过设定的保留策略(比如保留 7 天或达到一定大小)才会被删除。

当说"一个 Kafka 分区就是一个提交日志"时,它的意思是:一个分区就是一个只能在末尾追加消息、内部消息严格按 Offset 排序、并且不可更改的持久化数据结构。

8.3 为什么这个设计如此重要和强大?

理解了"分区即日志"后,就能明白 Kafka 许多强大功能的根源:

  • 高性能:向文件末尾追加内容是一种非常高效的磁盘操作(顺序写),速度远快于在任意位置读写(随机写)。这是 Kafka 实现高吞吐量的关键原因之一。
  • 解耦生产者和消费者:生产者只管往日志末尾扔消息,不用关心谁在消费、消费到哪了。消费者也只管根据自己的节奏,从日志的某个位置(Offset)开始读取,不影响生产者。日志成了它们之间完美的缓冲和解耦层。
  • 数据回溯与重放(Replayability):这是"提交日志"模式最强大的特性。由于日志是不可变的完整记录,消费者可以根据需要反复消费。
    • 一个消费者处理失败了,它可以回到之前的 Offset 重新处理。
    • 一个新的应用上线,它可以从头(Offset 0)开始读取全量历史数据,来构建自己的状态。
    • 可以同时有多个不同的应用(消费者组)从同一个日志中读取数据,用于不同的目的(一个用于实时监控,一个用于离线分析,一个用于数据同步),它们各自维护自己的消费位置(Offset),互不干扰。

9. Kafka 高性能原理

Kafka 把消息存储在 Log(日志)文件中,它的逻辑极其简单:

  • 只许追加,不许修改:Kafka 像记录账本一样,新消息永远只写在文件的末尾(Append Only),从不去修改文件中间的旧数据。
  • 避免寻道:因为永远是在末尾写,操作系统(OS)会非常聪明地把这一整块数据预留在缓存里。
  • 配合操作系统(Page Cache):Kafka 并不是直接把每条消息都"砰"地一下砸进硬盘,而是先写进操作系统的内存缓存(Page Cache)。OS 发现是在顺序写,就会利用预读和后写技术,在大背景下悄悄地、成批地把数据刷入硬盘。

总结:"高性能"是因为 Kafka 避开了硬盘最慢的"寻道"环节。它把硬盘当成了"内存"来用:

  • 普通数据库(如 MySQL):为了维护索引和更新数据,必须在硬盘各处跳跃读写(随机写)。
  • Kafka:像一条传送带,只管在末尾放东西(顺序写)。

这就是为什么即便是一台普通的服务器,Kafka 也能每秒处理几十万条消息的原因——它利用了"笨"硬盘做最擅长的事:埋头苦干(顺序追加)。


消息端到端生命周期

10. 副本机制

  • 主副本(Leader):负责处理所有客户端的读写请求。
  • 从副本(Follower):被动地从主副本同步数据,作为热备份。当主副本宕机时,会从中选举出新的主副本。

11. 消息流转全流程

在这里插入图片描述

11.1 通俗理解:快递物流系统

第一阶段:消息生产(快递寄出)

这里的"主角"是 Producer(生产者)。当点击"上传文件"时,Java 程序就是生产者。

  1. 创建消息:上传一个 PDF,系统把它包装成一个任务(Message)。
  2. 序列化:把 PDF 任务转换成计算机能懂的二进制"包裹"。
  3. 计算分区:就像选快递网点,Kafka 有很多"格子"(Partition),根据计算,系统决定把文件任务丢进编号为 P1 的格子里。
  4. 发送:Producer 把包裹发给 Broker 2(它是 P1 格子的"组长",术语叫 Leader)。
第二阶段:数据写入与同步(仓库备份)

这里发生在 Kafka 集群内部,目的是为了安全(防止数据丢失)。

  1. 写入本地:组长 Broker 2 把收到包裹存进自己的硬盘(顺序写,速度极快)。
  2. 同步消息:为了怕组长坏掉,组员 Broker 1(Follower)会跑过来把包裹克隆一份。
  3. 写入副本:组员也把包裹存好。
  4. 同步成功确认:组员告诉组长:“我备份好了!”
  5. 发送成功确认:组长确认大家都备份好了,给 Producer 回信:“寄送成功,包裹丢不了了!”(这就叫 Committed)。
第三阶段:消息消费(快递签收)

这里的"主角"是 Consumer(消费者),负责"解析 PDF 并调 AI 接口"的后台程序就是消费者。

  1. 加入消费组:消费者就像派送员,先去 Group Coordinator(调度员)那里报到。
  2. 分配分区:调度员告诉派送员:“你去负责盯 P1 这个格子的包裹。”
  3. 拉取消息(Fetch):派送员(Consumer)去 P1 格子那里看:“有没有新包裹?”
  4. 返回新消息:Broker 2 把还没处理的包裹丢给消费者。
  5. 反序列化并处理:派送员拆开包裹,开始真正的干活(比如:用 Apache Tika 读文字、调 AI 接口生成向量)。
  6. 提交位移(Commit):最重要的一步。干完活后,派送员在记事本上画个勾:"P1 格子的第 100 号包裹我已经处理完了!"这个记事本就是 Offset。下次重启后,派送员会从 101 号开始接着干,不会重复,也不会漏掉。

11.2 专业理解:Kafka 的高可用消息流转机制

将这个过程分为生产端的解耦发送、集群端的副本同步、消费端的位移管理三个专业环节来理解:

1. 消息生产阶段(Production & Partitioning)

涉及消息如何从应用层进入系统的过程:

  • Payload 构建:生产者(Producer)封装业务数据,形成包含 Key(用于分区 Hash)和 Value(业务报文)的 Record。
  • Serialization(序列化):将 Java 对象转换为字节流。
  • Partitioner(分区选择):根据 Key.hashCode() % NumPartitions 计算出该消息属于哪个物理分区(Partition)。
  • Leader Discovery(路由发现)
    • Producer 会定期拉取集群的 Metadata(元数据)——发消息的程序,每隔一会儿去拉取 Kafka 集群的地址登记表。
    • 得知 P1 分区的 Leader(主副本)位于 Broker 2。
    • 查表发现:1 号分区(Partition 1)的主干活节点,放在 2 号服务器(Broker 2)上。
    • 然后直接建立 TCP 连接发送 ProduceRequest——生产者直接连上 Broker 2 这台服务器,把消息发过去。
2. 数据可靠性保障(Replication & ISR)

展示 Kafka 如何保证 Durability(持久性)和 High Availability(高可用性):

  • Commit Log 顺序追加:Broker 2 接收到数据后,将其顺序写入磁盘的 .log 文件,并更新 LEO(Log End Offset)。
  • ISR(同步副本集)同步:Broker 1 作为 Follower(从副本),通过 FetchRequest 不断向 Leader 拉取数据,并将副本写入本地。
  • ACK 机制:配置了 acks=all。只有当 ISR 列表中所有的副本都成功写入并返回 ACK 后,Leader 才认为这条消息 Committed(已提交)。
  • High Watermark(高水位线):一旦消息提交,HW 指针向前移动。只有 HW 之前的消息对消费者是可见的,这保证了数据的一致性。
3. 消息消费阶段(Rebalance & Offset)

描述 Consumer Group(消费组)机制:

  • Group Coordination(组协调):消费者加入组时,Group Coordinator(调度员,由某个 Broker 担任)触发 Rebalance(重平衡),按照分配策略(如 Range 或 RoundRobin)将 P1 分配给消费者 C1。
  • Pull Model(拉取模型):消费者通过长轮询(Long Polling)发送 FetchRequest。它不需要 Leader 推送,而是根据自己的处理能力主动拉取。
  • Deserialization(反序列化):将二进制字节流还原为业务对象。
  • At-least-once Semantics(至少一次消费):消费者先执行业务逻辑(如调 AI 接口、写数据库),成功后向系统提交 Offset(位移)。
  • Offset Persistence:位移信息会被记录在 Kafka 内部特有的 Topic __consumer_offsets 中。这样即使 Consumer 重启,也能从 last_committed_offset 恢复,实现状态保持。

11.3 术语解析

术语 说明
Broker Kafka 集群里的一台服务器。Broker1、Broker2、Broker3 就是三台独立服务器。
Topic(主题) 可以理解成一个大文件夹,不同任务不同 Topic。
Partition(分区) 把大文件夹拆成好几个小文件夹,比如 Partition1、Partition2。
Leader(主副本) 每个小分区(Partition)都会存多份防止丢数据,真正负责收消息、发消息的那一份就叫 Leader。
Producer(生产者) 发消息的客户端 / 程序。
Metadata(元数据) Kafka 集群的地址登记表,记录着每个分区的 Leader 在哪台服务器上。
Leader Discovery(路由发现) 生产者查登记表,找到分区主节点在哪的过程。

Kafka Producer

12. 生产者发送过程

在 Kafka 中,把产生消息的那一方称为生产者。比如打开淘宝的那一刻,登录信息、登录次数都会作为消息传输到 Kafka 后台;浏览购物时,浏览信息、搜索指数、购物爱好都会作为一个个消息传递给 Kafka 后台,然后淘宝会根据爱好做智能推荐。那么这些生产者产生的消息是怎么传到 Kafka 应用程序的呢?发送过程是怎么样的呢?

尽管消息的产生非常简单,但是消息的发送过程还是比较复杂的。整体流程如下:

  1. 创建 ProducerRecord 对象:ProducerRecord 是 Kafka 中的一个核心类,它代表了一组 Kafka 需要发送的 key/value 键值对,由记录要发送到的主题名称(Topic Name)、可选的分区号(Partition Number)以及可选的键值对构成。
  2. 序列化:在发送 ProducerRecord 时,需要将键值对对象由序列化器转换为字节数组,这样它们才能够在网络上传输。然后消息到达了分区器。
  3. 分区选择
    • 如果发送过程中指定了有效的分区号,那么在发送记录时将使用该分区。
    • 如果发送过程中未指定分区,则将使用 Key 的 Hash 函数映射指定一个分区。
    • 如果发送的过程中既没有分区号也没有 Key,则将以循环的方式分配一个分区。
  4. 时间戳:ProducerRecord 还有关联的时间戳,如果用户没有提供时间戳,那么生产者将会在记录中使用当前的时间作为时间戳。Kafka 最终使用的时间戳取决于 Topic 主题配置的时间戳类型:
    • 如果将主题配置为使用 CreateTime,则生产者记录中的时间戳将由 Broker 使用。
    • 如果将主题配置为使用 LogAppendTime,则生产者记录中的时间戳在将消息添加到其日志中时,将由 Broker 重写。
  5. 批次发送:消息被存放在一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上,由一个独立的线程负责把它们发到 Kafka Broker 上。
  6. 响应处理:Kafka Broker 在收到消息时会返回一个响应:
    • 写入成功:返回一个 RecordMetaData 对象,包含了主题和分区信息,以及记录在分区里的偏移量,时间戳类型也会返回给用户。
    • 写入失败:返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败的话,就返回错误消息。

13. 解密 Kafka 生产者的"魔幻数字 5"

13.1 引言:一个"魔幻"的配置限制

在配置 Kafka 生产者以实现更高级别的可靠性时,常常会遇到一个看似神秘的规则:当开启幂等性(enable.idempotence=true)时,max.in.flight.requests.per.connection 这个参数的值必须小于或等于 5。

为什么是 5?不是 4,也不是 10?这个数字从何而来?

这并非一个随意的"魔幻数字",而是 Kafka 为了在性能和资源消耗之间取得精妙平衡而做出的一个非常务实的工程决策。要理解它,首先要明白,这个限制的根源不在于生产者,而在于 Broker。

13.2 问题的核心:幂等性与 Broker 的"记忆成本"

当设置 enable.idempotence=true 时,核心诉求是:消息绝不能因为网络重试而重复写入

为了满足这个要求,Broker 端必须承担起"去重"的责任。它需要记住它最近处理过的消息,以便在生产者因为网络超时等原因重发同一条消息时,能够识别并丢弃这个重复的请求。

但是,Broker 的内存是有限的,它不可能为成千上万个生产者永久地记住所有历史消息。因此,Broker 必须有一个"记忆窗口"——它只承诺会记住最近的一小部分消息序列。

13.3 逻辑链条

链条 1:目标——绝对不许重复(幂等性)

在分布式系统中,最怕的就是"重试导致的重复"。

  • 场景:发了一个包裹给 Kafka,Kafka 确实收到了,但由于网络瞬间断开,确认回执(ACK)没传回来。
  • 结果:以为没发成功,于是又发了一次。Kafka 此时有两份一模一样的包裹。
  • 解决方案:开启"幂等性"(Idempotence)。给每个包裹贴上序号(1, 2, 3…),Kafka 收到包裹后,如果发现 1 号已经收过了,就直接扔掉第二个 1 号。
链条 2:代价——服务器的"记忆力"(Broker 的格子)

Kafka 为了实现上面的"去重",必须在内存里开辟空间记录:“某某人最近发了哪些序号的包裹”。

  • 问题:Kafka 要面对成千上万个发送者。如果每个发送者寄过的所有序号它都记着,服务器内存会瞬间爆炸。
  • 决策:Kafka 决定只记最近的一小部分。
  • 硬性规定:Kafka 的源码里设定,服务器只预留 5 个格子来记每个人的最近发货记录。这就是那个"记忆窗口 = 5"。
链条 3:限制——客户端的"发货速度"(In-Flight Requests)

现在"记忆力"只有 5,如果你是发送者,该怎么配合?

  • 参数含义max.in.flight.requests.per.connection 指的是:在还没收到回执的时候,最多允许多少个包裹同时在路上飞?
  • 逻辑冲突
    • 设为 1:路上一永远只有一个包裹。最安全,但慢得像蜗牛。
    • 设为 5:路上有 1, 2, 3, 4, 5 号包裹。万一 1 号丢了,重发 1 号,服务器翻开本子一看:“1, 2, 3, 4, 5 都在我记录范围内,1 号我收过了,滚!”——去重成功!
    • 设为 6(出事了!):路上有 1, 2, 3, 4, 5, 6 号包裹。服务器收到了 1 到 5 号,本子记满了。服务器收到 6 号时,因为本子只有 5 个格,它必须忘掉 1 号才能记下 6 号。这时,如果 1 号因为网络问题重发了,服务器翻开本子(现在记的是 2, 3, 4, 5, 6):“咦?没见过 1 号啊,这肯定是个新包裹,进来吧!”——灾难:服务器存了两份 1 号数据。幂等性失效!

13.4 结语:一个务实的工程权衡

因为 Kafka 服务器(Broker)为了省内存,只承诺帮发送者记住最近 5 个发货序号;所以,作为发送者(Producer),在还没得到服务器确认之前,最多只能一口气发 5 个包裹,否则服务器的脑子就转不过来(记不住),会导致数据重复。

数字 5,便是在这个权衡中被选定的一个足够在"允许一定的并发以提升吞吐量"和"不过度消耗 Broker 内存资源"之间取得良好平衡的工程实践值。它让这个强大的幂等性功能,得以在资源可控的前提下,稳定而高效地实现。


14. 创建生产者

14.1 创建 Kafka 生产者(代码逐行解释)

// 1. 创建配置对象
private Properties properties = new Properties();

// 2. 填写 3 个必选配置
properties.put("bootstrap.servers", "broker1:9092,broker2:9092");
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// 3. 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

逐行大白话翻译:

代码 说明
new Properties() 创建一个配置本,专门用来填 Kafka 的参数
bootstrap.servers 填 Kafka 服务器地址,写 2 台是防止一台宕机连不上
key.serializer / value.serializer Kafka 只认识字节数组,不认识文字。这个配置就是把字符串(比如 “France”)转成字节,给 Kafka 用
new KafkaProducer<>(properties) 拿着填好的配置本,创建一个真正的生产者对象,这个 producer 就是用来发消息的

14.2 三种消息发送方式

发送消息前,必须先创建消息包裹:ProducerRecord——这就是要发给 Kafka 的快递包裹,里面装着:主题、键、内容。

方式 1:简单发送(发后即忘)
// 创建消息包裹:主题=CustomerCountry,键=West,内容=France
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "West", "France");
// 发送消息
producer.send(record);
  • 大白话:打包好快递 → 直接扔给 Kafka → 不管有没有送到
  • 优点:最快
  • 缺点:消息丢了都不知道
  • 适用:日志、监控数据(丢了也无所谓)
方式 2:同步发送(发完等待结果)
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "West", "France");
try {
    // 发送消息 + 等待 Kafka 回复(卡住!)
    RecordMetadata recordMetadata = producer.send(record).get();
} catch (Exception e) {
    // 发送失败打印错误
    e.printStackTrace();
}
  • 大白话:打包快递 → 发出去 → 死等 Kafka 签收。等到签收才发下一个,期间程序卡住不动
  • 优点:知道消息是否成功
  • 缺点:极慢,高并发场景卡死
  • 适用:简单测试、不重要的小业务
方式 3:异步发送(发完不等待,结果出来通知你)
// 1. 创建消息
ProducerRecord<String, String> record = new ProducerRecord<>("CustomerCountry", "Huston", "America");

// 2. 发送消息 + 绑定回调(成功/失败自动通知)
producer.send(record, new DemoProducerCallBack());

// 3. 回调类:Kafka 处理完自动调用这个方法
class DemoProducerCallBack implements Callback {
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if (exception != null) {
            // 失败:打印错误
            exception.printStackTrace();
        }
    }
}
  • 大白话:打包快递 → 发出去 → 程序继续干别的,不等待。Kafka 签收成功/失败后,自动打电话通知你(回调)
  • 优点:速度快 + 能感知失败(企业最常用)
  • 缺点:代码稍微多一点

15. 分区机制

15.1 顺序轮询(Round Robin)

顺序分配,消息是均匀地分配给每个 Partition,即每个分区存储一次消息。轮询策略是 Kafka Producer 提供的默认策略,如果不使用指定的分区策略的话,Kafka 默认会使用顺序轮询策略的方式。

15.2 随机轮询(Random)

随机轮询简而言之就是随机地向 Partition 中保存消息。

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());

先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。

本质上随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。

15.3 按 Key 进行消息保存(Key-Ordering)

这个策略也叫做 key-ordering 策略。Kafka 中每条消息都会有自己的 Key,一旦消息被定义了 Key,就可以保证同一个 Key 的所有消息都进入到相同的分区里面。由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略

文章参考:

1.Kafka 学习指南
2.学习 Kafka 入门知识看这一篇就够了!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐