面试必备-如何保证kakfa消息有序?

178 阅读5分钟

Kafka的有序性大作战:生产者、Broker、消费者的“三国演义”

大家好,我是你们的“技术相声演员”,今天我们来聊聊 Kafka 如何保证消息的有序性。说到有序性,你是不是想到了排队买奶茶的场景?没错,Kafka 的消息有序性就像奶茶店的排队系统,生产者是点单的小哥,Broker 是制作奶茶的机器,消费者是取奶茶的顾客。今天我们就从这三个角色的角度,看看 Kafka 是如何让消息“排队”的。


1. 什么是有序性?为什么它重要?

有序性,简单来说就是消息按照发送的顺序被处理。比如你发了一条朋友圈:“今天吃了火锅,真香!”然后紧接着又发了一条:“哦不对,是烧烤。”如果这两条消息的顺序颠倒了,你的朋友们可能会以为你是个“烧烤火锅傻傻分不清”的吃货。

在分布式系统中,保证消息的有序性非常重要,尤其是在金融、订单处理等场景中,顺序错了可能会导致严重的后果。比如你转账 100 元,然后又转账 200 元,如果顺序颠倒了,可能会导致账户余额变成负数(银行:你礼貌吗?)。


2. 生产者的有序性:点单小哥的“手速”

2.1 生产者如何保证消息有序?

Kafka 的生产者就像奶茶店的点单小哥,他的任务是快速、准确地把顾客的订单(消息)传递给制作奶茶的机器(Broker)。为了保证消息的有序性,生产者需要做到以下几点:

  1. 单分区写入:Kafka 的消息有序性是针对单个分区(Partition)而言的。也就是说,如果你希望一组消息有序,必须把它们发送到同一个分区。比如你把“火锅”和“烧烤”两条消息发送到同一个分区,Kafka 会保证它们的顺序。
  2. 重试机制:生产者在发送消息时可能会遇到网络抖动或 Broker 宕机的情况。Kafka 提供了重试机制,但要注意,如果重试时消息顺序乱了,可能会导致有序性被破坏。为了避免这个问题,可以设置 max.in.flight.requests.per.connection=1,确保同一时间只有一个请求在传输。

image.png image.png

2.2 代码示例

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("max.in.flight.requests.per.connection", 1); // 保证有序性

Producer<String, String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("my-topic", "key1", "今天吃了火锅,真香!"));
producer.send(new ProducerRecord<>("my-topic", "key1", "哦不对,是烧烤。")); // 保证这两条消息有序

3. Broker 的有序性:奶茶制作机的“流水线”

3.1 Broker 如何保证消息有序?

Broker 是 Kafka 的核心,它负责存储和分发消息。为了保证消息的有序性,Broker 需要做到以下几点:

  1. 分区(Partition)机制:Kafka 的每个 Topic 可以分为多个分区,每个分区是一个有序的消息队列。消息在分区内是严格有序的,但在不同分区之间是无序的。所以,如果你希望消息全局有序,只能使用一个分区(但这会牺牲并发性能)。
  2. 日志追加(Append-Only Log) :Kafka 的消息是以追加的方式写入分区的,这意味着消息一旦写入,顺序就不会改变。就像奶茶店的订单一旦被打印出来,顺序就固定了。
  3. 副本同步(Replication) :Kafka 的每个分区都有多个副本,其中一个 Leader 副本负责读写,其他 Follower 副本负责同步。为了保证有序性,Follower 副本必须严格按照 Leader 副本的顺序同步消息。

image.png

3.2 思考题

如果 Kafka 的分区数设置为 1,能保证全局有序性,但性能会下降。你会如何权衡有序性和性能?


4. 消费者的有序性:取奶茶的“排队规则”

4.1 消费者如何保证消息有序?

消费者是最后一步,它需要从 Broker 中读取消息并处理。为了保证消息的有序性,消费者需要做到以下几点:

  1. 单线程消费:每个分区只能由一个消费者线程消费。如果多个线程同时消费一个分区,可能会导致消息处理顺序混乱。就像奶茶店如果让多个顾客同时取同一杯奶茶,场面一定会很混乱。
  2. 偏移量(Offset)管理:Kafka 会为每个分区的消息分配一个唯一的偏移量(Offset),消费者需要按照偏移量的顺序处理消息。如果消费者处理失败,可以从上次的偏移量重新开始消费。
  3. 消费者组(Consumer Group) :Kafka 的消费者组机制允许多个消费者并行消费不同分区的消息。如果 Topic 有多个分区,消费者组中的每个消费者会负责一个或多个分区,从而保证每个分区内的消息有序。

image.png

4.2 代码示例

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("my-topic"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
    }
}

5. 总结:Kafka 有序性的“三国演义”

  • 生产者:通过单分区写入和限制并发请求,保证消息发送有序。
  • Broker:通过分区机制、日志追加和副本同步,保证消息存储有序。
  • 消费者:通过单线程消费和偏移量管理,保证消息处理有序。

Kafka 的有序性就像一场“三国演义”,生产者、Broker 和消费者各司其职,共同维护消息的顺序。虽然 Kafka 的设计牺牲了部分全局有序性,但通过合理的分区和消费者组配置,我们可以在性能和有序性之间找到平衡。


6. 思考题

  1. 如果 Kafka 的某个分区发生故障,如何保证消息不丢失且有序?
  2. 在消费者端,如果某个消息处理失败,如何避免影响后续消息的处理?

好了,今天的“Kafka 有序性大作战”就到这里。如果你觉得这篇文章有用,记得点赞、分享,顺便去喝杯奶茶放松一下(记得按顺序点单哦)!下次再见!