Kafka的数据时如何保证数据不重复,不丢失?

生产者

  • ACK + 重试机制
    • 生产者生产数据写入kafka,等待kafka返回ack确认,收到ack,生产者发送下一条

    • 选项

      • 0:不等待ack,直接发送下一条

        • 优点:快
        • 缺点:数据易丢失
      • 1:生产者将数据写入Kafka,Kafka等待这个分区Leader副本,返回ack,发送下一条

        • 优点:性能和安全做了中和的选项
        • 缺点:依旧存在一定概率的数据丢失的情况
      • all:生产者将数据写入Kafka,Kafka等待这个分区所有副本同步成功,返回ack,发送下一条

        • 优点:安全
        • 缺点:性能比较差
        • 方案:搭配min.insync.replicas来使用
          • min.insync.replicas:表示最少同步几个副本就可以返回ack
      • 重试机制

      retries = 0 发送失败的重试次数
      

消费者

  • 基础规则:每个消费者消费Topic分区的数据按照Offset进行消费
  • 第一次消费:根据属性
    • auto.offset.reset:lastest/earliest
  • 第二次消费开始:消费者在内存中记录上一次消费的offset + 1 = 这一次要消费的位置
  • Kafka将每个消费者消费的位置主动定期记录在一个Topic中:__consumer_offsets
  1. 如果消费者遇到故障,Kafka怎么保证不重复不丢失?
    • Kafka将每个消费者消费的offset存储在一个独立的Topic中:__consumer_offsets
    • 如果消费者故障,重启,从这个Topic查询上一次的offset + 1
    • 问题2:这个Topic中记录的offset是怎么来的?
      • 默认机制:根据时间周期由消费者自动提交
      • 导致问题:数据重复或者数据丢失问题
      • 解决问题:根据处理的结果来实现基于每个分区的手动提交
        • 消费一个分区、处理一个分区、处理成功,提交这个分区的offset
    • 工作中:不需要管使用手动提交还是自动提交?
      • 因为工作中不利用Kafka的存储来实现offset的恢复
      • 一般我们自己存储offset:将每个分区的offset存储在MySQL、Zookeeper、Redis
      • 如果程序故障,从MySQL、Zookeeper、Redis中读取上一次的Offset

自动提交

 Properties props = new Properties();
        props.setProperty("bootstrap.servers", "node1:9092");
        props.setProperty("group.id", "test");
        props.setProperty("enable.auto.commit", "true");
        props.setProperty("auto.commit.interval.ms", "1000");
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

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

        consumer.subscribe(Arrays.asList("bigdata01"));

        while (true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : poll) {
                String topic = record.topic();
                int partition = record.partition();
                long offset = record.offset();
                String key = record.key();
                String value = record.value();
                System.out.println(topic+"\t"+partition+"\t"+offset+"\t"+key+"\t"+value);
            }
        }

原因

  • 数据丢失的情况
    • 如果刚消费,还没处理,就达到提交周期,记录了当前 的offset
    • 最后处理失败,需要重启,重新消费处理
    • Kafka中已经记录消费过了,从上次消费的后面进行消费
  • 数据重复的情况
    • 如果消费并处理成功,但是没有提交offset,程序故障
    • 重启以后,kafka中记录的还是之前的offset,重新又消费一遍
    • 数据重复问题

解决

  • 实现手动提交Topic的Offset

小结

  • 消费是否成功,是根据处理的结果来决定的
  • 提交offset是根据时间来决定了
  • 需要:根据处理的结果来决定是否提交offset
    • 如果消费并处理成功:提交offset
    • 如果消费处理失败:不提交offset

手动提交Topic Offset

Properties props = new Properties();
        props.setProperty("bootstrap.servers", "node1:9092");
        props.setProperty("group.id", "test");
        props.setProperty("enable.auto.commit", "false");
//        props.setProperty("auto.commit.interval.ms", "1000");
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

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

        while (true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : poll) {
                System.out.println(record.topic()+"\t"+record.partition()+"\t"+record.key()+"\t"+record.offset()+"\t"+record.value());
            }
            consumer.commitSync();
        }

原因

  • Offset是分区级别的
  • 提交offset是按照整个Topic级别来提交的

解决

  • 提交offset的时候,按照分区来提交
  • 消费成功一个分区,就提交一个分区的offset

小结

  • 导致问题:数据重复
  • 导致原因:offset是分区级别,提交时topic级别,只要有一个分区失败,整个提交失败,实际上部分分区已经处理成功了

手动提交partition Offset

Properties props = new Properties();
        props.setProperty("bootstrap.servers", "node1:9092");
        props.setProperty("group.id", "test");
        props.setProperty("enable.auto.commit", "false");
//        props.setProperty("auto.commit.interval.ms", "1000");
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

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

        TopicPartition part0 = new TopicPartition("bigdata01", 0);
        TopicPartition part1 = new TopicPartition("bigdata01", 1);

        ArrayList<TopicPartition> list = new ArrayList<>();
        list.add(part0);
        list.add(part1);

//        consumer.assign(Arrays.asList(part0,part1));
        consumer.assign(list);

        while (true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));

            Set<TopicPartition> partitions = poll.partitions();

            for (TopicPartition partition : partitions) {
                List<ConsumerRecord<String, String>> records = poll.records(partition);

                long offset = 0;
                for (ConsumerRecord<String, String> record : records) {
                    String topic = record.topic();
                    int part= record.partition();
                    offset = record.offset();
                    String key = record.key();
                    String value = record.value();
                    System.out.println(topic+"\t"+part+"\t"+offset+"\t"+key+"\t"+value);
                }
                Map<TopicPartition, OffsetAndMetadata> offsets = Collections.singletonMap(partition,new OffsetAndMetadata(offset+1));
                consumer.commitSync();
            }
        }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值