Kafka 如何保证不重复消费

224 阅读5分钟

Kafka 防重指南:如何避免消息被“吃”两次?

大家好,我是你们的 Kafka 导游,今天我们要聊一个既严肃又搞笑的话题:如何避免消息被重复消费。想象一下,你点了一份外卖,结果送餐小哥给你送了两份一模一样的披萨,你是开心还是崩溃?在 Kafka 的世界里,重复消费就像这种“双倍快乐”,但往往带来的不是惊喜,而是灾难。

今天,我们就从生产者Broker消费者三个角度,全方位解析 Kafka 如何保证消息不重复消费。准备好了吗?系好安全带,我们出发!


1. 生产者:别让你的消息“分身”

生产者的任务是发送消息,但如果发送过程中出了问题,比如网络抖动或者生产者崩溃,可能会导致同一条消息被发送多次。为了避免这种情况,Kafka 提供了两种机制:

(1)幂等性(Idempotence)

幂等性就像你给朋友发消息,发一次和发一百次效果是一样的。Kafka 的生产者可以通过设置 enable.idempotence=true 来开启幂等性。开启后,Kafka 会为每条消息分配一个唯一的序列号(Sequence Number),Broker 会根据序列号去重,确保同一条消息不会被重复写入。

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("enable.idempotence", true); // 开启幂等性

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

(2)事务(Transactions)

如果你需要发送多条消息,并且希望它们要么全部成功,要么全部失败,可以使用 Kafka 的事务功能。事务就像你去超市购物,要么把所有商品都买下来,要么一件都不买。

props.put("transactional.id", "my-transactional-id"); // 设置事务 ID
producer.initTransactions(); // 初始化事务

try {
    producer.beginTransaction();
    producer.send(new ProducerRecord<>("my-topic", "key1", "value1"));
    producer.send(new ProducerRecord<>("my-topic", "key2", "value2"));
    producer.commitTransaction(); // 提交事务
} catch (ProducerFencedException e) {
    producer.close();
} catch (KafkaException e) {
    producer.abortTransaction(); // 回滚事务
}

2. Broker:消息的“守门员”

Broker 是 Kafka 的核心,负责存储和转发消息。它的任务是确保消息不丢失、不重复。Broker 主要通过以下方式避免消息重复:

(1)消息去重

如果生产者开启了幂等性,Broker 会为每个分区维护一个序列号缓存。当收到新消息时,Broker 会检查序列号,如果发现重复,就直接丢弃。

(2)日志压缩(Log Compaction)

Kafka 的日志压缩功能可以确保每个键(Key)只保留最新的值。这对于需要精确一次语义的场景非常有用。比如,如果你的消息键是用户 ID,日志压缩可以确保每个用户只保留最新的状态。

# 开启日志压缩
log.cleanup.policy=compact

3. 消费者:别“吃”到重复的消息

消费者是消息的最终接收者,但如果处理不当,可能会导致消息被重复消费。为了避免这种情况,我们可以从以下几个方面入手:

(1)手动提交偏移量(Offset)

Kafka 消费者默认是自动提交偏移量的,但这可能会导致消息被重复消费。比如,消费者处理完消息但还没来得及提交偏移量就崩溃了,重启后就会重新消费这些消息。

为了避免这种情况,我们可以改为手动提交偏移量,确保消息处理完成后再提交。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("enable.auto.commit", "false"); // 关闭自动提交
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());
    }
    consumer.commitSync(); // 手动提交偏移量
}

(2)幂等消费

消费者也可以通过实现幂等性来避免重复消费。比如,在处理消息时,先检查这条消息是否已经处理过。如果是,就直接跳过。

Set<String> processedMessages = new HashSet<>(); // 用于记录已处理的消息

for (ConsumerRecord<String, String> record : records) {
    if (processedMessages.contains(record.key())) {
        continue; // 如果已经处理过,跳过
    }
    // 处理消息
    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
    processedMessages.add(record.key()); // 记录已处理的消息
}

(3)使用外部存储去重

如果你的消息量非常大,可以使用外部存储(如 Redis 或数据库)来记录已处理的消息。每次消费消息时,先检查外部存储,确保消息没有被重复处理。


总结:Kafka 防重的“三板斧”

  1. 生产者:开启幂等性和事务,确保消息不重复发送。
  2. Broker:通过序列号去重和日志压缩,确保消息不重复存储。
  3. 消费者:手动提交偏移量、实现幂等消费或使用外部存储去重,确保消息不重复处理。

通过这三个环节的紧密配合,Kafka 可以最大限度地避免消息被重复消费。就像一场精密的接力赛,每个环节都不能掉链子。


最后的思考

Kafka 的防重机制就像一场精心设计的“防重阵法”,需要生产者、Broker 和消费者共同努力。但即便如此,仍然无法 100% 保证消息不重复。在实际应用中,我们需要根据业务场景,选择合适的防重策略。

就像哪吒的混天绫,虽然威力无穷,但也需要灵活运用。希望今天的文章能让你对 Kafka 的防重机制有更深的理解。如果你有任何问题或想法,欢迎在评论区留言!

我是你们的 Kafka 导游,下次再见!🚀