***实现Kafka完全不重复消费或者丢失消费

1. 存在原因:丢失消费/重复消费

1 自动提交offset:
	1.1 当自动提交时间为1s时,间隔时间达到1s,offset(100)已经提交,但是数据处理尚未完成(只处理了80)出错了(挂了),此时从新启动后会从已经提交的offset(100)开始消费处理,那么81-100这些数据就未处理,导致丢失消费
	1.2 当自动提交时间为3s时,数据1s已经处理完了一批,突然挂了,由于提交时间未到,offset未提交,重新启动时,会重复处理已经处理过的数据,导致重复消费
2 官方手动提交(与上雷同问题)
	2.1 同步手动提交
	//同步提交,当前线程会阻塞 直到提交成功才会 继续消费后面的数据 效率低下 一般不用
    //consumer.commitSync();
	2.2 异步手动提交
	//异步手动提交
    consumer.commitAsync(new OffsetCommitCallback() {
        @Override
        public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
            if( null != exception){
                exception.printStackTrace();
            }
        }
    });

2. 自定义存储offset

实现方案:
	1 实现offset重新分配的机制
	2 保证数据处理与offset提交能事务性
实现过程:
	1 借助ConsumerRebalanceListener类,重写重新分配offset的方法以及提交offset的方法
	2 结果mysql,实现事务性

3. 代码实现

package com.dream.bigdata.bi.es.kafka;

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

import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;

/**
 * 手动提交offset
 */
public class MyManualCommitOffsetKafkaConsumer {
    
    

    private static String group = "qdd";
    private static String topic = "qdd100";

    /**
     * 1. 自动提交offset:
     *      1.1 当间隔时间到达自动提交时间时,offset已提交,但是
     *
     * 自定义mysql数据列:group | topic | partition | offset (前三个作为联合主键)
     * @param args
     */
    public static void main(String[] args) {
    
    

        // 1. 创建消费者配置信息
        Properties properties = new Properties();

        // 2. Kafka集群信息
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "113.143.100.155:9092,113.143.100.140:9092,113.143.100.148:9092");

        // 3. ***** 关闭自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        // 4. 自动提交时间
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000); // 默认提交时间1000ms

        // 5. key、value的反序列化方式
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");

        // 6. 消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, group);

        // ** offset重置配置生效条件:1 当Kafka中没有初始化的offset(换组消费) 2 当Kafka保存消息已经没有当前offset的数据
        // 面试题:如何重新消费一个主题的数据?答:设置消费者offset重置为earliest,换组消费
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        // 7. 创建消费者
        KafkaConsumer consumer = new KafkaConsumer(properties);

        // 8. 消费者订阅主题 此处借助于ConsumerRebalanceListener 监听分区offset发生变化后的重新分配机制
        consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
    
    

            // rebalance之前将记录进行保存
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    
    
                for (TopicPartition partition : partitions) {
    
    
                    // 获取分区
                    int sub_topic_partition_id = partition.partition();
                    // 对应分区的偏移量
                    long sub_topic_partition_offset = consumer.position(partition);
                    String date = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss")
                            .format(new Date(new Long(System.currentTimeMillis())));

                    DBUtils.update("replace into offset values(?,?,?,?,?)",
                            new Offset(
                                    group,
                                    topic,
                                    sub_topic_partition_id,
                                    sub_topic_partition_offset,
                                    date
                            )
                    );
                }
            }

            // rebalance之后读取之前的消费记录,继续消费
            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    
    
                for (TopicPartition partition : partitions) {
    
    
                    int sub_topic_partition_id = partition.partition();
                    long offset = DBUtils.queryOffset(
                            "select sub_topic_partition_offset from offset where consumer_group=? and sub_topic=? and sub_topic_partition_id=?",
                            group,
                            topic,
                            sub_topic_partition_id
                    );
                    System.out.println("partition = " + partition + "offset = " + offset);
                    // 定位到最近提交的offset位置继续消费
                    consumer.seek(partition, offset);

                }
            }
        });

        while (true) {
    
    
            // 9. 获取数据
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
            consumedata(consumerRecords);

            //同步手动提交,当前线程会阻塞 直到提交成功才会 继续消费后面的数据 效率低下 一般不用
            //consumer.commitSync();

            //异步手动提交
//            consumer.commitAsync(new OffsetCommitCallback() {
    
    
//                @Override
//                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
    
    
//                    if( null != exception){
    
    
//                        exception.printStackTrace();
//                    }
//                }
//            });

        }

    }
    @Transactional(rollbackFor = Exception.class)
    public void consumedata(ConsumerRecords<String, String> consumerRecords){
    
    
        List<Offset> offsets = new ArrayList<>();
        // 10. 解析并打印结果
        for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
    
    
            System.out.println(consumerRecord.key() + " ===> " + consumerRecord.value());
            String date = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss").format(
                    new Date(
                            new Long(
                                    System.currentTimeMillis()
                            )
                    )
            );
            offsets.add(new Offset(group, topic, consumerRecord.partition(), consumerRecord.offset(), date));

            System.out.println("|---------------------------------------------------------------\n" +
                    "|group\ttopic\tpartition\toffset\ttimestamp\n" +
                    "|" + group + "\t" + topic + "\t" + consumerRecord.partition() + "\t" + consumerRecord.offset() + "\t" + consumerRecord.timestamp() + "\n" +
                    "|---------------------------------------------------------------"
            );
        }
        // 异步批量插入offset
        for (Offset offset : offsets) {
    
    
            DBUtils.update("replace into offset values(?,?,?,?,?)", offset);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/q18729096963/article/details/114274468