微服务架构中的 Kafka:异步通信与服务解耦(二)

三、Kafka 基础入门

3.1 Kafka 是什么

Kafka 最初由 LinkedIn 公司开发,是一个开源的分布式事件流平台,后成为 Apache 基金会的顶级项目 。它不仅仅是一个简单的消息队列,更是一个分布式流处理平台,具备强大的消息队列、存储系统和流处理能力。Kafka 采用发布 - 订阅的消息模型,能够以极高的吞吐量处理海量的消息,并且支持将消息持久化到磁盘,确保数据的可靠性和可回溯性。

Kafka 的设计目标主要包括以下几个方面:

  • 高吞吐量:Kafka 在设计上充分考虑了性能因素,通过一系列优化手段,如顺序读写、零拷贝技术等,使其能够在普通的商用硬件上实现单机每秒处理 100K 条以上消息的传输,甚至在集群环境下,吞吐量可以达到每秒百万级别的消息处理能力,这使得它非常适合处理大规模的实时数据。
  • 持久化存储:Kafka 将所有消息都持久化到磁盘上,通过高效的日志存储机制,保证消息的不丢失。它采用追加式的日志写入方式,减少了磁盘寻道时间,提高了写入性能。同时,Kafka 还支持数据的多副本复制,进一步增强了数据的可靠性,即使部分节点出现故障,也不会导致数据丢失。
  • 分布式与扩展性:Kafka 基于分布式架构,一个 Kafka 集群可以由多个 Kafka 服务器(Broker)组成,这些服务器可以分布在不同的物理节点上,实现水平扩展。通过分区(Partition)机制,Kafka 可以将一个大的主题(Topic)分布到多个 Broker 上,从而提高系统的处理能力和负载均衡能力。当业务量增长时,可以方便地添加新的 Broker 节点来扩展集群的容量和性能。
  • 顺序保证:对于同一个分区内的消息,Kafka 能够严格保证它们的顺序性,这在一些对消息顺序有严格要求的场景中非常重要,如金融交易记录、日志记录等。虽然跨分区的消息顺序无法保证,但在很多实际应用中,通过合理的分区设计,可以满足大部分业务对消息顺序的需求。
  • 支持多种客户端和语言:Kafka 提供了丰富的客户端库,支持 Java、C/C++、Scala、Python、Go、Erlang、Node.js 等多种编程语言,方便不同技术栈的开发者在各自的项目中集成和使用 Kafka。

3.2 Kafka 的核心概念

3.2.1 生产者(Producer)

生产者是 Kafka 中负责向 Kafka 集群发送消息的客户端应用程序。它可以将各种类型的数据,如日志信息、业务事件、实时监控数据等,封装成 Kafka 消息,并发送到指定的主题(Topic)中。

在实际应用中,生产者通常会根据业务需求对消息进行一些配置和处理。例如,在一个电商系统中,订单服务作为生产者,当有新订单生成时,它会将订单相关的信息(如订单号、用户 ID、商品信息、订单金额等)封装成 Kafka 消息,并发送到名为 "order - topic" 的主题中。生产者在发送消息时,可以设置消息的键(Key),键的作用主要有两个方面:一是可以用于决定消息被发送到主题的哪个分区,通过对键进行哈希计算,可以将具有相同特征的消息发送到同一个分区,从而保证这些消息在分区内的顺序性;二是在一些场景下,消费者可以根据键来进行消息的筛选和处理,提高处理效率。此外,生产者还可以设置消息的一些属性,如消息的时间戳、压缩方式等,以满足不同的业务需求。生产者发送消息的过程可以是同步的,也可以是异步的。同步发送时,生产者会等待 Kafka 集群的确认响应,确保消息成功发送后才继续执行后续操作;异步发送时,生产者将消息发送到缓冲区后就立即返回,由后台线程负责将缓冲区中的消息批量发送到 Kafka 集群,这种方式可以提高发送效率,适用于对实时性要求不高但对吞吐量要求较高的场景。

3.2.2 消费者(Consumer)

消费者是从 Kafka 集群接收并处理消息的客户端应用程序。它通过订阅一个或多个主题,获取主题中的消息,并按照业务逻辑进行相应的处理。

消费者在 Kafka 中是以消费者组(Consumer Group)的形式存在的。一个消费者组可以包含多个消费者实例,这些消费者实例共同消费一个或多个主题中的消息。在同一个消费者组内,每个消费者负责消费不同分区的数据,这样可以实现消息的并行处理,提高消费效率。例如,假设有一个名为 "user - behavior - topic" 的主题,用于记录用户在网站上的行为数据,它有 4 个分区。现在有一个消费者组,包含 3 个消费者实例。Kafka 会将这 4 个分区分配给这 3 个消费者实例,可能的分配方式是消费者实例 1 消费分区 1 和分区 2,消费者实例 2 消费分区 3,消费者实例 3 消费分区 4。这样,3 个消费者实例可以同时从各自负责的分区中读取消息并进行处理,大大提高了消息的消费速度。

消费者组的这种设计模式使得 Kafka 既支持消息的广播(Broadcast)模式,也支持消息的单播(Unicast)模式。当所有消费者都属于同一个消费者组时,消息会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理,这就实现了单播模式,类似于点对点的消息传递;当所有消费者都属于不同的消费者组时,消息会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就实现了广播模式,类似于发布 - 订阅模式。

消费者在消费消息时,采用的是拉(Pull)模式,即消费者主动从 Kafka 集群中拉取消息。这种模式与推(Push)模式相比,具有更好的灵活性和可控性,消费者可以根据自身的处理能力来调整拉取消息的频率和数量,避免因为生产者推送消息的速度过快而导致消费者处理不过来的情况。同时,消费者还需要维护一个消费位移(Offset),它表示消费者在分区中已经消费到的位置。每次消费者成功消费消息后,会将消费位移提交给 Kafka 集群,以便在下次消费时能够从上次消费的位置继续读取消息,保证消息的顺序消费和不重复消费。

3.2.3 主题(Topic)

主题是 Kafka 中消息的逻辑分类,它类似于传统消息队列中的队列概念,但又有所不同。每个主题可以看作是一个独立的消息通道,生产者将消息发送到特定的主题,而消费者通过订阅感兴趣的主题来获取消息。

在实际应用中,主题通常根据业务功能或数据类型来划分。例如,在一个社交媒体平台中,可能会有 "user - registration - topic" 主题用于记录用户注册信息,"post - creation - topic" 主题用于记录用户发布的帖子内容,"comment - topic" 主题用于记录用户对帖子的评论等。通过将不同类型的消息划分到不同的主题中,可以方便地对消息进行管理和处理,提高系统的可维护性和可扩展性。

一个 Kafka 集群可以包含多个主题,每个主题可以有多个分区,并且可以为每个主题设置不同的配置参数,如消息的保留时间、副本数量等。生产者在发送消息时,需要指定消息所属的主题;消费者在订阅消息时,也需要指定要订阅的主题。这样,通过主题的概念,Kafka 实现了消息的分类管理和灵活订阅,使得不同的业务模块可以独立地进行消息的生产和消费,而互不干扰。

3.2.4 分区(Partition)

分区是主题的物理细分,每个主题可以包含一个或多个分区。分区的主要作用是提高 Kafka 的并行处理能力和数据可靠性。

从并行处理的角度来看,当一个主题有多个分区时,生产者发送的消息会被分布到不同的分区中,消费者组中的不同消费者实例可以同时从不同的分区中读取消息并进行处理,从而实现了消息的并行消费,大大提高了系统的吞吐量。例如,一个电商系统的 "order - topic" 主题有 10 个分区,当有大量订单生成时,生产者可以将订单消息均匀地分布到这 10 个分区中。同时,一个包含 5 个消费者实例的消费者组可以分别从不同的分区中读取订单消息进行处理,这样可以在短时间内处理大量的订单消息,满足高并发场景下的业务需求。

从数据可靠性的角度来看,每个分区都可以有多个副本(Replica),这些副本分布在不同的 Kafka 服务器(Broker)上。其中,一个副本被选举为领导者(Leader)副本,其他副本为跟随者(Follower)副本。生产者发送的消息会首先被写入到 Leader 副本中,然后 Follower 副本会从 Leader 副本中同步数据,以保持数据的一致性。当 Leader 副本所在的 Broker 出现故障时,Kafka 会从 Follower 副本中选举出一个新的 Leader 副本,继续提供服务,从而保证了数据的可靠性和系统的高可用性。

此外,分区还可以根据键(Key)来进行数据的分配。如果生产者在发送消息时指定了键,Kafka 会根据键的哈希值将消息分配到相应的分区中,这样可以保证具有相同键的消息会被发送到同一个分区,从而满足一些对消息顺序有要求的业务场景。例如,在一个订单处理系统中,将订单 ID 作为消息的键,那么同一个订单的所有相关消息(如订单创建、订单支付、订单发货等)都会被发送到同一个分区,消费者在消费这些消息时,可以按照顺序处理,确保订单处理的正确性和一致性。

四、Kafka 实现异步通信

4.1 异步通信原理

Kafka 的异步通信基于生产者 - 消费者模型和发布 - 订阅模型。在这个模型中,生产者负责将消息发送到 Kafka 集群的指定主题(Topic),而消费者则从感兴趣的主题中订阅并获取消息进行处理。

当生产者有消息要发送时,它并不会直接将消息发送给消费者,而是将消息发送到 Kafka 集群的主题中。Kafka 集群会将这些消息持久化存储在磁盘上,并根据分区(Partition)策略将消息分配到不同的分区中。生产者发送消息的过程是异步的,它在将消息发送到 Kafka 集群后,不需要等待 Kafka 集群的确认响应,就可以继续执行其他操作。这大大提高了生产者的发送效率,减少了因为等待响应而造成的时间浪费,使得生产者能够在短时间内发送大量的消息,提高了系统的吞吐量。

消费者则通过订阅主题来获取消息。消费者可以以消费者组(Consumer Group)的形式存在,同一个消费者组内的不同消费者实例可以同时从同一个主题的不同分区中读取消息并进行处理,实现了消息的并行消费。消费者在消费消息时,采用的是拉(Pull)模式,即消费者主动从 Kafka 集群中拉取消息。消费者会定期向 Kafka 集群发送拉取请求,Kafka 集群根据消费者的请求,将相应分区中的消息返回给消费者。消费者接收到消息后,根据自身的业务逻辑进行处理。处理完成后,消费者会将消费位移(Offset)提交给 Kafka 集群,以便在下次消费时能够从上次消费的位置继续读取消息,保证消息的顺序消费和不重复消费。

这种异步通信机制实现了生产者和消费者之间的解耦。生产者不需要关心消费者是否能够及时接收和处理消息,它只需要将消息发送到 Kafka 集群即可;消费者也不需要关心消息是由哪个生产者发送的,它只需要从 Kafka 集群中获取自己感兴趣的消息进行处理。通过 Kafka 这个中间层,生产者和消费者之间的依赖关系被大大降低,它们可以独立地进行扩展、升级和维护,而不会相互影响。

4.2 Kafka 异步通信的优势

4.2.1 解耦服务

在微服务架构中,各个微服务之间往往存在复杂的依赖关系。如果采用同步通信方式,一个微服务的接口发生变化,可能会导致依赖它的其他微服务也需要进行相应的修改,这增加了系统的维护成本和风险。而 Kafka 的异步通信机制通过消息队列将微服务之间的直接调用转换为间接的消息传递,实现了服务之间的解耦。每个微服务只需要关注自己的业务逻辑和与 Kafka 的交互,不需要关心其他微服务的实现细节和接口变化。例如,在一个电商系统中,订单服务在创建订单后,将订单消息发送到 Kafka 的 "order - topic" 主题中,库存服务和物流服务通过订阅该主题来获取订单消息并进行相应的处理。当订单服务的业务逻辑发生变化,或者需要对订单消息的格式进行调整时,只需要在订单服务内部进行修改,而不会影响到库存服务和物流服务的正常运行,因为它们是通过 Kafka 的消息队列进行通信的,彼此之间没有直接的依赖关系。

4.2.2 削峰填谷

在一些业务场景中,系统可能会面临突发的高并发请求,例如电商平台的促销活动、社交媒体平台的热点事件等。如果采用同步通信方式,系统可能会因为无法及时处理大量的请求而导致性能下降甚至崩溃。Kafka 的异步通信机制可以很好地解决这个问题,它通过消息队列的缓冲作用,将大量的请求消息暂时存储起来,然后在系统负载较低时,再将消息逐步发送给消费者进行处理,实现了削峰填谷的功能。例如,在电商促销活动期间,短时间内会产生大量的订单请求,订单服务将这些订单消息发送到 Kafka 的 "order - topic" 主题中,Kafka 集群可以存储这些大量的订单消息。库存服务和物流服务可以根据自身的处理能力,从 Kafka 集群中逐步拉取订单消息进行处理,避免了因为瞬间高并发请求而导致系统负载过高的问题,保证了系统的稳定性和可靠性。

4.2.3 提升系统响应速度

由于 Kafka 的异步通信机制使得生产者在发送消息后不需要等待消费者的响应,就可以继续执行其他操作,这大大提高了系统的响应速度。在一些对响应时间要求较高的业务场景中,如用户注册、下单等操作,用户希望能够尽快得到系统的反馈。通过使用 Kafka 进行异步通信,生产者可以快速地将消息发送到 Kafka 集群,然后立即返回响应给用户,而不需要等待相关业务逻辑的全部处理完成。例如,在一个在线教育平台中,用户进行课程购买操作时,订单服务将订单消息发送到 Kafka 集群后,立即返回 "订单提交成功" 的响应给用户,后续的课程分配、支付处理等操作由其他微服务通过订阅 Kafka 消息来异步完成。这样,用户能够在最短的时间内得到系统的响应,提升了用户体验,同时也提高了系统的整体处理能力,因为各个微服务可以在后台并行地处理消息,而不会相互阻塞。

4.3 代码示例

以下是使用 Java 语言实现 Kafka 生产者和消费者的代码示例,并对关键配置和消息收发逻辑进行解释。

首先,需要在项目中引入 Kafka 的客户端依赖。如果使用 Maven 项目,可以在pom.xml文件中添加以下依赖:

复制代码

<dependencies>

<dependency>

<groupId>org.apache.kafka</groupId>

<artifactId>kafka-clients</artifactId>

<version>3.1.0</version>

</dependency>

</dependencies>

4.3.1 Kafka 生产者代码示例
复制代码

import org.apache.kafka.clients.producer.*;

import java.util.Properties;

import java.util.concurrent.ExecutionException;

public class KafkaProducerExample {

public static void main(String[] args) throws ExecutionException, InterruptedException {

// Kafka集群地址

String bootstrapServers = "localhost:9092";

// 要发送到的主题

String topic = "test-topic";

// 配置生产者属性

Properties props = new Properties();

// Kafka集群地址

props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);

// 消息的键序列化器

props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

// 消息的值序列化器

props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

// 确认机制,"all"表示等待所有副本确认

props.put(ProducerConfig.ACKS_CONFIG, "all");

// 创建生产者实例

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// 要发送的消息

String key = "key1";

String value = "Hello, Kafka!";

// 创建消息记录

ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value);

try {

// 发送消息,并同步获取发送结果

RecordMetadata metadata = producer.send(record).get();

System.out.println("Message sent successfully: " +

"Topic: " + metadata.topic() +

", Partition: " + metadata.partition() +

", Offset: " + metadata.offset());

} catch (InterruptedException | ExecutionException e) {

System.out.println("Failed to send message: " + e.getMessage());

} finally {

// 关闭生产者

producer.close();

}

}

}

关键配置解释

  • BOOTSTRAP_SERVERS_CONFIG:指定 Kafka 集群的地址,生产者通过该地址与 Kafka 集群建立连接。
  • KEY_SERIALIZER_CLASS_CONFIG和VALUE_SERIALIZER_CLASS_CONFIG:分别指定消息的键和值的序列化器,用于将消息转换为字节数组发送到 Kafka 集群。这里使用StringSerializer将字符串类型的键和值进行序列化。
  • ACKS_CONFIG:指定生产者的确认机制。"all" 表示生产者在发送消息后,需要等待所有副本都确认收到消息后才认为发送成功,这可以保证消息的可靠性,但会降低发送的性能;"1" 表示生产者只需要等待领导者副本确认收到消息即可;"0" 表示生产者发送消息后不需要等待任何确认。

消息发送逻辑解释

  1. 创建Properties对象,设置生产者的配置属性。
  1. 使用配置属性创建KafkaProducer实例。
  1. 创建ProducerRecord对象,指定要发送的主题、消息的键和值。
  1. 使用producer.send(record).get()方法发送消息,并通过get()方法同步获取发送结果。如果发送成功,get()方法会返回一个RecordMetadata对象,包含消息发送到的主题、分区和偏移量等信息;如果发送失败,会抛出InterruptedException或ExecutionException异常。
  1. 最后,在finally块中关闭生产者,释放资源。
4.3.2 Kafka 消费者代码示例
复制代码

import org.apache.kafka.clients.consumer.*;

import java.time.Duration;

import java.util.Collections;

import java.util.Properties;

public class KafkaConsumerExample {

public static void main(String[] args) {

// Kafka集群地址

String bootstrapServers = "localhost:9092";

// 消费者组ID

String groupId = "test-group";

// 要订阅的主题

String topic = "test-topic";

// 配置消费者属性

Properties props = new Properties();

// Kafka集群地址

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);

// 消费者组ID

props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);

// 消息的键反序列化器

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");

// 消息的值反序列化器

props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");

// 自动提交位移的时间间隔

props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

// 自动提交位移

props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");

// 创建消费者实例

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

// 订阅主题

consumer.subscribe(Collections.singletonList(topic));

try {

while (true) {

// 拉取消息,每次拉取等待100毫秒

ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));

for (ConsumerRecord<String, String> record : records) {

System.out.println("Message received: " +

"Topic: " + record.topic() +

", Partition: " + record.partition() +

", Offset: " + record.offset() +

", Key: " + record.key() +

", Value: " + record.value());

}

}

} finally {

// 关闭消费者

consumer.close();

}

}

}

关键配置解释

  • BOOTSTRAP_SERVERS_CONFIG:指定 Kafka 集群的地址,消费者通过该地址与 Kafka 集群建立连接。
  • GROUP_ID_CONFIG:指定消费者组 ID,同一个消费者组内的消费者共同消费订阅主题的消息。
  • KEY_DESERIALIZER_CLASS_CONFIG和VALUE_DESERIALIZER_CLASS_CONFIG:分别指定消息的键和值的反序列化器,用于将从 Kafka 集群接收到的字节数组转换为具体的对象类型。这里使用StringDeserializer将字节数组反序列化为字符串类型的键和值。
  • AUTO_COMMIT_INTERVAL_MS_CONFIG:指定自动提交消费位移的时间间隔,单位为毫秒。消费者在拉取并处理消息后,会按照这个时间间隔自动将消费位移提交给 Kafka 集群。
  • ENABLE_AUTO_COMMIT_CONFIG:设置是否开启自动提交消费位移功能,"true" 表示开启,"false" 表示关闭。如果关闭自动提交,需要手动调用consumer.commitSync()或consumer.commitAsync()方法来提交消费位移。

消息接收逻辑解释

  1. 创建Properties对象,设置消费者的配置属性。
  1. 使用配置属性创建KafkaConsumer实例。
  1. 使用consumer.subscribe(Collections.singletonList(topic))方法订阅指定的主题,这里Collections.singletonList(topic)表示将主题封装成一个单元素的列表。
  1. 在一个无限循环中,使用consumer.poll(Duration.ofMillis(100))方法拉取消息,每次拉取等待 100 毫秒。poll()方法会返回一个ConsumerRecords对象,包含拉取到的所有消息。
  1. 遍历ConsumerRecords对象,获取每个ConsumerRecord,并打印消息的相关信息,包括主题、分区、偏移量、键和值。
  1. 最后,在finally块中关闭消费者,释放资源。
相关推荐
苏格拉没有底_coder36 分钟前
Redis+Kafka实现动态延时任务
数据库·redis·kafka
颜颜颜yan_2 小时前
【HarmonyOS5】掌握UIAbility启动模式:Singleton、Specified、Multiton
后端·架构·harmonyos
一块plus3 小时前
Polkadot 的 Web3 哲学:从乔布斯到 Gavin Wood 的数字自由传承
人工智能·程序员·架构
it_xiao_xiong3 小时前
微服务集成seata分布式事务 at模式快速验证
分布式·微服务·架构
seventeennnnn3 小时前
Java面试实战:Spring Boot+微服务+AI的谢飞机闯关之路 | CSDN博客精选
spring boot·redis·spring cloud·微服务·ai·java面试·rag
互联网搬砖老肖3 小时前
Web 架构之 Kubernetes 弹性伸缩策略设计
前端·架构·kubernetes
小傅哥4 小时前
Docker 环境配置(一键安装)
后端·架构
蚂蚁数据AntData4 小时前
品牌形象全面升级|Apache Fory:破界新生,开启高性能序列化新纪元
架构·开源·apache
DemonAvenger5 小时前
Go 内存分析工具链:从开发到生产环境
性能优化·架构·go