Kafka 8:消费者手动提交偏移量(解决消息丢失与重复消费问题)

1.消费者提交偏移量导致的问题
  • 当我们调用 poll 方法的时候,broker 返回的是生产者写入 Kafka 但是还没有被消费者读取过的记录,消费者可以使用 Kafka 来追踪消息在分区里的位 置,我们称之为偏移量。消费者更新自己读取到哪个消息的操作,我们称之为提交
  • 消费者是如何提交偏移量的呢?消费者会往一个叫做_consumer_offset 的特殊主题发送一个消息,里面会包括每个分区的偏移量。发生了再均衡之后, 消费者可能会被分配新的分区,为了能够继续工作,消费者者需要读取每个分区最后一次提交的偏移量,然后从指定的地方,继续做处理。
    1)如果提交的偏移量小于消费者实际处理的最后一个消息的偏移量,处于两个偏移量之间的消息会被重复处理
    2)如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失
  • 所以, 处理偏移量的方式对客户端会有很大的影响 。KafkaConsumer API 提供了很多种方式来提交偏移量 。
    在这里插入图片描述
2.提交偏移量的方式
2.1 自动提交
  • 最简单的提交方式是让消费者自动提交偏移量。 如果 enable.auto.comnit 被设为 true,消费者会自动把从 poll()方法接收到的最大偏移量提交上去。 提交时间间隔由 auto.commit.interval.ms 控制,默认值是 5s。自动提交是在轮询里进行的,消费者每次在进行轮询时会检査是否该提交偏移量了,如果是, 那么就会提交从上一次轮询返回的偏移量。
  • 不过,在使用这种简便的方式之前,需要知道它将会带来怎样的结果。
  • 假设我们仍然使用默认的 5s 提交时间间隔, 在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。 这个时候偏移量已经落后了 3s,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量, 减小可能出现重复消息的 时间窗, 不过这种情况是无法完全避免的 。
  • 在使用自动提交时,每次调用轮询方法都会把上一次调用返回的最大偏移量提交上去,它并不知道具体哪些消息已经被处理了,所以在再次调用之前最 好确保所有当前调用返回的消息都已经处理完毕(enable.auto.comnit 被设为 true 时,在调用 close()方法之前也会进行自动提交)。一般情况下不会有什么 问题,不过在处理异常或提前退出轮询时要格外小心。
  • 自动提交虽然方便,但是很明显是一种基于时间提交的方式,不过并没有为我们留有余地来避免重复处理消息
2.2 手动同步提交
  • 我们通过控制偏移量提交时间来消除丢失消息的可能性,并在发生再均衡时减少重复消息的数量。消费者 API 提供了另一种提交偏移量的方式,开发 者可以在必要的时候提交当前偏移量,而不是基于时间间隔。
  • 把 auto.commit. offset 设为 false,自行决定何时提交偏移量。使用 commitsync()提交偏移量最简单也最可靠。这个方法会提交由 poll()方法返回的最 新偏移量,提交成功后马上返回,如果提交失败就抛出异常。
  • 注意: commitsync()将会提交由 poll()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitsync(),否则还是会有丢失消息的风险。如果 发生了再均衡,从最近批消息到发生再均衡之间的所有消息都将被重复处理。
  • 只要没有发生不可恢复的错误,commitSync()方法会阻塞,会一直尝试直至提交成功,如果失败,也只能记录异常日志。
  • 创建主题simple,"./kafka-topics.sh --zookeeper localhost:2181 --create --topic simple --replication-factor 1 --partitions 8",创建消费者CommitSyncConsumer
    在这里插入图片描述
    在这里插入图片描述
package org.example.commit;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;

import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

/**
 *手动提交偏移量,生产者使用同步发送
 **/
public class CommitSyncConsumer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitSync");
        //取消自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        try {
            consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
                            ";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
                    //do your work
                }
                //同步提交(这个方法会阻塞)
                consumer.commitSync();
            }
        } finally {
            consumer.close();
        }
    }
}
2.3 异步提交
  • 手动提交时,在 broker 对提交请求作出回应之前,应用程序会一直阻塞。这时我们可以使用异步提交 API,我们只管发送提交请求,无需等待 broker 的响应。
  • 在成功提交或碰到无法恢复的错误之前, commitsync()会一直重试,但是 commitAsync 不会。它之所以不进行重试,是因为在它收到服务器响应的时候, 可能有一个更大的偏移量已经提交成功。
  • 假设我们发出一个请求用于提交偏移量 2000,这个时候发生了短暂的通信问题,服务器收不到请求,自然也不会作出任何响应。与此同时,我们处理了另 外一批消息,并成功提交了偏移量 3000。如果 commitAsync()重新尝试提交偏移量 2000,它有可能在偏移量 3000 之后提交成功。这个时候如果发生再均衡, 就会出现重复消息。
  • commitAsync()也支持回调,在 broker 作出响应时会执行回调。回调经常被用于记录提交错误或生成度量指标。
  • 创建消费者CommitAsyncConsumer
package org.example.commit;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;

import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

/**
 *异步手动提交偏移量,生产者使用同步发送
 **/
public class CommitAsyncConsumer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitAsync");
        //取消自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        try {
            consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
                            ";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
                    //do your work
                }
                //异步提交
                consumer.commitAsync();
                //允许执行回调,对提交失败的消息进行处理
                consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        if (e != null) {
                            System.out.println("Commit failed for offsets");
                            System.out.println(map);
                            e.printStackTrace();
                        }
                    }
                });
            }
        } finally {
            consumer.close();
        }
    }
}

2.4 同步和异步组合
  • 因为同步提交一定会成功、异步可能会失败,所以一般的场景是同步和异步一起来做。
  • 一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。
  • 因此,在消费者关闭前一般会组合使用 commitAsync()和 commitSync()。
  • 创建消费者SyncAndAsync
package org.example.commit;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;

import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

/**
 *同步和异步组合
 **/
public class SyncAndAsync {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "SyncAndAsync");
        //取消自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        try {
            consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
                            ";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
                    //do your work
                }
                //异步提交
                consumer.commitAsync();
            }
        } catch (Exception e) {
            System.out.println("Commit failed");
            e.printStackTrace();
        } finally {
            try {
                //同步提交
                consumer.commitSync();
            } finally {
                consumer.close();
            }
        }
    }
}

2.5 特定提交
  • 在我们前面的提交中,提交偏移量的频率与处理消息批次的频率是一样的。但如果想要更频繁地提交该怎么办?
  • 如果 poll()方法返回一大批数据,为了避免因再均衡引起的重复处理整批消息,想要在批次中间提交偏移量该怎么办?这种情况无法通过调用 commitSync()或 commitAsync()来实现,因为它们只会提交最后一个偏移量,而此时该批次里的消息还没有处理完。
  • 消费者 API 允许在调用 commitsync()和 commitAsync()方法时传进去希望提交的分区和偏移量的 map。
  • 创建消费者CommitSpecial
package org.example.commit;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;

import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 *特定提交
 **/
public class CommitSpecial {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitSpecial");
        //取消自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        Map<TopicPartition, OffsetAndMetadata> currOffsets = new HashMap<>();
        int count = 0;
        try {
            consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
                            ";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
                    //do your work
                    currOffsets.put(new TopicPartition(record.topic(), record.partition()),
                            new OffsetAndMetadata(record.offset() + 1, "no meta"));
                    if (count % 11 == 0) {
                        consumer.commitAsync(currOffsets,null);
                    }
                    count++;
                }
            }
        } finally {
            consumer.commitSync();
            consumer.close();
        }
    }
}
2.6 创建生产者ProducerCommit
package org.example.commit;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import org.example.config.BusiConst;

import java.util.Properties;

public class ProducerCommit {
    public static void main(String[] args) {
        //生产者必须指定3个属性(broker地址清单,key和value的序列化器)
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "192.168.42.111:9092");
        properties.put("key.serializer", StringSerializer.class);
        properties.put("value.serializer", StringSerializer.class);
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        try {
            ProducerRecord<String, String> record;
            for (int i = 0; i < 10; i++) {
                record = new ProducerRecord<String, String>(BusiConst.CONSUMER_COMMIT_TOPIC,
                        String.valueOf(i), "Fisher" + i);
                //发送并忘记
                producer.send(record);
                System.out.println(i + ", message is sent.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            producer.close();
        }
    }
}

2.7 先启动消费者,再启动生产者,生产10条消息

在这里插入图片描述

2.8 查看消费者打印

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值