kafka 从分区任意位置、分区开头、分区末尾开始消费数据

最近就kafka消费者消费数据时,消费者提交的offset与同事们有一些分歧和讨论,这里记录一下自己的研究。

我们知道redis和kafka都可以作为消息队列使用,都可以完成发布订阅功能,但是kafka相较于redis可以实现订阅消息的存储,可以实现订阅消息的任意位置消费,更重要的时kafka订阅消息是可以存储到磁盘上的,而redis订阅消息是无法存储磁盘的。

(1)消费者消费数据时加入一个消费者分组之后,可以通过 subscribe函数订阅某个topic,这时这个消费者进入brokers的group management管理机制,同一个分片只能被一个分组中的消费者消费,如果同一个分片希望被多个消费者消费,需要将多个消费者放入到不同的消费者分组中。

 //订阅指定的topic
consumer.subscribe(Arrays.asList(topic));
(2)还有一种消费数据的方式是可以通过assign函数指定要消费的分区数据,这种方式可以指定从分区的任意位置开始消费数据,当然这种
//消费者指定要消费的分区,指定分区之后消费者崩溃之后 不会引发分区reblance
consumer.assign(list);
消费数据的方式,如果消费者奔溃之后,不会引发分区reblance,也就是说assign的consumer不会拥有kafka的group management机制。
我们上面说过,同一个分片只能由消费者分组中的同一个消费者进行消费,假设当消费者A使用assign指定分区进行消费时,如果这时消费者A使用的分组group B,是通过subscribe订阅了这个主题的分片时,由于消费者A不加入group management,它相当于一个独立的临时消费者,这时消费者A也是可以正常消费的,看起来就是一个分片被一个消费者组中的多个消费者消费一样。

(3)我们还可以配置如下属性auto.offset.reset来,设置消费者从分区的开头或者末尾进行消费数据。当然这也是有条件的。


 //一般配置earliest 或者latest 值
props.put("auto.offset.reset", "latest");

我把上述三种情况的消费者不同使用方式下,消费者提交offset的情况进行了归总和说明:


早在kafka0.8.2.2版本的时候,kafka已经支持消息offset存在brokers中,只不过默认是将offset存储到zookeeper中。kafka现在最新发布的版本都是默认将数据存储到brokers中。我的代码示例是使用了kafka0.10.0.0版本,当我们这里通过assign函数分配指定的分区时


下面是我的测试代码,有兴趣的同学可以查看和验证上述结论:

 

/**
 * 
 * @author yujie.wang
 * kafka生产者示例代码
 */
public class Producer_Sample {
    //kafka集群机器
    private static final String KAFKA_HOSTS = "10.4.30.151:9092,10.4.30.151:9093,10.4.30.151:9094";
    //topic名称
    private static final String TOPIC = "my-replicated-topic_2";
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Producer_Sample producer = new Producer_Sample();
        producer.producer_send(TOPIC);
        System.out.println("end");
    }
    
    /**
     * 生产者生产数据
     * 发送消息是异步进行,一旦消息被保存到分区缓存中,send方法就返回
     * 一旦消息被接收 就会调用callBack
     * @param topic
     */
    public void producer_send(String topic){
        Properties props = new Properties();
        //kafka集群机器
        props.put("bootstrap.servers", KAFKA_HOSTS);
        //生产者发送的数据需要等待主分片和其副本都保存才发回确认消息
        props.put("acks", "all");
        //生产者发送失败后的确认消息
        props.put("retries", 0);
        //生产者 每个分区缓存大小 16K
        props.put("batch.size", 16384);
        //生产者发送分区缓存中数据前停留时间
        props.put("linger.ms", 1);
        //生产者可用缓存总量大小 32M
        props.put("buffer.memory", 33554432);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
         
        Producer<String, String> producer = new KafkaProducer<String,String>(props);
        for(int i = 220; i < 230; i++){
            //发送消息是异步进行,一旦消息被保存到分区缓存中,send方法就返回
            // producer.send(new ProducerRecord<String, String>("my-replicated-topic_1", Integer.toString(i), Integer.toString(i)));
            producer.send(new ProducerRecord<String, String>(topic, "call___"+Integer.toString(i+20), "call___"+Integer.toString(i)),
                    new Call());
            System.out.println("send return I: "+ i);
        }
 
        producer.close();
    }
 
    /**
     *消息被保存之后的回调方法
     */
    class Call implements Callback{
 
        @Override
        public void onCompletion(RecordMetadata recordmetadata,
                Exception exception) {
            // TODO Auto-generated method stub
            System.out.println("callBack: "+ recordmetadata.checksum() + " recordmetadata content : "+recordmetadata.toString());
        }
        
    }
}


/**
 * @author yujie.wang
 * kafka消费者示例,包含随机位置消费和最多一次消费方式
 * 消费者提交消费数据offset 分为自动提交和手动控制提交
 * 
 * 这份代码示例中包含了 多种从kafka的任意位置获取数据的方式
 */
public class Consumer_Sample {
 
    //kafka集群机器
    private static final String KAFKA_HOSTS = "10.4.30.151:9092,10.4.30.151:9093,10.4.30.151:9094";
    //topic名称
    private static final String TOPIC = "my-replicated-topic_2";
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Consumer_Sample consumer = new Consumer_Sample();
        //从分区的末尾 或者已存在groupid的请情况下从未消费位置开始消费数据
        consumer.consumerSubscribe("true", TOPIC);
        // 通过实现ConsumerRebalanceListener接口 进而时间任意位置的消费
        consumer.consumerSubscribeImplListener("true", TOPIC);
        //从指定的分区  开始位置seekToBeginning 或者任意位置seek消费数据
        consumer.consumerAssin("true", TOPIC);
        //通过配置属性auto.offset.reset 来设置消费者从分区开头或者末尾进行消费,但是需要使用一定条件的group Id
        consumer.consumerAutoOffsetReset("true", TOPIC);
        System.out.println("consumer end");
    }
    
    
    /**
     * 直接通过订阅一个指定分区来消费数据
     * (1)如果该groupId消费者分组下 有消费者提交过offset,则从 当前提交的offset位置开始消费数据
     * (2)如果该groupId消费者分组下 没有有消费者提交过offset,则从 当前log添加的最后位置(也就是数据的末尾)开始消费数据
     * @param isAutoCommitBool
     * @param topic
     */
    public void consumerSubscribe(final String isAutoCommitBool, final String topic){
         Properties props = new Properties();
         //配置kafka集群机器
         props.put("bootstrap.servers", KAFKA_HOSTS);
         //消费者分组
         props.put("group.id", "yujie37");
         //这里设置 消费者自动提交已消费消息的offset
         props.put("enable.auto.commit", isAutoCommitBool);
         // 设置自动提交的时间间隔为1000毫秒
         props.put("auto.commit.interval.ms", "1000");
         // 设置每次poll的最大数据个数
         props.put("max.poll.records", 5);
         props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
         //订阅topic
         consumer.subscribe(Arrays.asList(topic));
         List<PartitionInfo> parList = consumer.partitionsFor(topic);
 
         //打印出分区信息
         printPartition(parList);
 
         //消费数据
         while (true) {
             ConsumerRecords<String, String> records = consumer.poll(5000);
             System.out.println("topic: "+topic + " pool return records size: "+ records.count());
             for (ConsumerRecord<String, String> record : records){
                  System.out.println(record.toString());
                   //手动提交已消费数据的offset
                  if("false".equalsIgnoreCase(isAutoCommitBool)){
                     consumer.commitSync();
                  }
                 
             }
           
         }
    }
    
    
    /**
     * 
     * @param isAutoCommitBool true 开启自动提交offset;false 不开启
     * @param topic
     * (1)如果该groupId消费者分组下 有消费者提交过offset,则从 当前提交的offset位置开始消费数据
     * (2)如果该groupId消费者分组下 没有有消费者提交过offset,则从 当前log添加的最后位置(也就是数据的末尾)开始消费数据
     * 
     * 注意如果enable.auto.commit 设置为false,如果消费完数据没有提交已消费数据的offset,
     * 则会出现重复消费数据的情况
     * 
     * 通过实现ConsumerRebalanceListener接口中的onPartitionsAssigned方法,并在其中调用消费者的seek或者seekToBeginning
     * 方法定位分区的任意位置或者开头位置
     */
    public void consumerSubscribeImplListener(final String isAutoCommitBool, final String topic){
         Properties props = new Properties();
         //配置kafka集群机器
         props.put("bootstrap.servers", KAFKA_HOSTS);
         //消费者分组
         props.put("group.id", "yujie26");
         //这里设置 消费者自动提交已消费消息的offset
         props.put("enable.auto.commit", isAutoCommitBool);
         // 设置自动提交的时间间隔为1000毫秒
         props.put("auto.commit.interval.ms", "1000");
         // 设置每次poll的最大数据个数
         props.put("max.poll.records", 5);
         props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
         //订阅topic,并实现ConsumerRebalanceListener
         consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener(){
            @Override
            public void onPartitionsRevoked(//分区撤销时,消费者可以向该分区提交自己当前的offset
                    Collection<TopicPartition> collection) {
                // TODO Auto-generated method stub
                if("false".equalsIgnoreCase(isAutoCommitBool)){
                    //consumer.commitSync();
                }
            }
 
            @Override
            public void onPartitionsAssigned(//当分区分配给消费者时,消费者可以通过该方法重新定位需要消费的数据位置
                    Collection<TopicPartition> collection) {
                // TODO Auto-generated method stub
                //将消费者定位到各个分区的开始位置进行消费
        /*        consumer.seekToBeginning(collection);
                System.out.println("seek beg");*/
            
                Iterator it = collection.iterator();
                while(it.hasNext()){
                    //将消费者定位到指定分区的指定位置7进行消费
                    consumer.seek((TopicPartition)it.next(), 7);
                }
                
            }
         });
         while (true) {
             ConsumerRecords<String, String> records = consumer.poll(5000);
             System.out.println("topic: "+topic + "pool return records size: "+ records.count());
             for (ConsumerRecord<String, String> record : records){
                  System.out.println(record.toString());
                   //手动提交已消费数据的offset
                  if("false".equalsIgnoreCase(isAutoCommitBool)){
                     consumer.commitSync();
                  }
                 
             }
           
         }
    }
    
    
    /**
     * 
     * @param isAutoCommitBool true 开启自动提交offset;false 不开启
     * @param topic
     * 如果groupId之前存在 , 则从之前提交的最后消费数据的offset处继续开始消费数据
     * 如果groupId之前不存在,则从当前分区的最后位置开始消费
     * 
     * 注意如果enable.auto.commit 设置为false,如果消费完数据没有提交已消费数据的offset,
     * 则会出现重复消费数据的情况
     */
    public void consumerAutoOffsetReset(final String isAutoCommitBool, final String topic){
         Properties props = new Properties();
         //配置kafka集群机器
         props.put("bootstrap.servers", KAFKA_HOSTS);
         //消费者分组
         props.put("group.id", "yujie32");
         //这里设置 消费者自动提交已消费消息的offset
         props.put("enable.auto.commit", isAutoCommitBool);
         // 设置自动提交的时间间隔为1000毫秒
         props.put("auto.commit.interval.ms", "1000");
         // 设置每次poll的最大数据个数
         props.put("max.poll.records", 5);
         //设置使用最开始的offset偏移量为该group.id的最早。如果不设置,则会是latest即该topic最新一个消息的offset
         //如果采用latest,消费者只能得道其启动后,生产者生产的消息
         //一般配置earliest 或者latest 值
         props.put("auto.offset.reset", "latest");
         props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
         //订阅topic,并实现ConsumerRebalanceListener
         consumer.subscribe(Arrays.asList(topic));
         while (true) {
             ConsumerRecords<String, String> records = consumer.poll(5000);
             System.out.println("topic: "+topic + "pool return records size: "+ records.count());
             for (ConsumerRecord<String, String> record : records){
                  System.out.println(record.toString());
                   //手动提交已消费数据的offset
                  if("false".equalsIgnoreCase(isAutoCommitBool)){
                     consumer.commitSync();
                  }
                 
             }
           
         }
    }
 
    
    /**
     * 通过assign分配的分区,消费者发生故障 Server端不会触发分区重平衡(即使该消费者共享某个已有的groupId),每个消费者都是独立工作的
     * 为了避免offset提交冲突,需要确保每个消费者都有唯一的groupId
     * 从指定的分区的开头开始消费数据
     * @param isAutoCommitBool true 开启自动提交offset;false 不开启
     * @param topic
     */
    public void consumerAssin(String isAutoCommitBool,String topic){
         Properties props = new Properties();
         //配置kafka集群机器
         props.put("bootstrap.servers", KAFKA_HOSTS);
         //消费者分组
         props.put("group.id", "yujie35");
         //这里设置 消费者自动提交已消费消息的offset
         props.put("enable.auto.commit", isAutoCommitBool);
         // 设置自动提交的时间间隔为1000毫秒
         props.put("auto.commit.interval.ms", "1000");
         // 设置每次poll的最大数据个数
         props.put("max.poll.records", 5);
         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);
         //获得topic的所有分区
         List<PartitionInfo> parList = consumer.partitionsFor(topic);
         //打印出分区信息
         printPartition(parList);
         
         List<TopicPartition> list = new ArrayList<TopicPartition>();
         for(PartitionInfo par : parList){
             TopicPartition partition = new TopicPartition(topic, par.partition());
             list.add(partition);
         }
         //消费者指定要消费的分区,指定分区之后消费者崩溃之后 不会引发分区reblance
         consumer.assign(list);
 
         //从list中所有分区的开头开始消费数据,这个操作不改变已提交的消费数据的offset
         // consumer.seekToBeginning(list);
     
 
     /*    for(TopicPartition tpar:list ){
             //consumer.seek(tpar, position);
         } */
     
 
         while (true) {
             ConsumerRecords<String, String> records = consumer.poll(5000);
             System.out.println("topic: "+topic + " pool return records size: "+ records.count());
             for (ConsumerRecord<String, String> record : records){
                  System.out.println(record.toString());
                   //手动提交已消费数据的offset
                  if("false".equalsIgnoreCase(isAutoCommitBool)){
                     consumer.commitSync();
                  }
                 
             }
           
         }
    }
    
 
    
    
 
    
    public void printPartition(List<PartitionInfo> parList){
        for(PartitionInfo p : parList){
            System.out.println(p.toString());
        }
    }
    
    /**
     * 单独处理每个分区中的数据,处理完了之后异步提交offset,注意提交的offset是程序将要读取的下一条消息的offset
     * @param consumer
     */
    public void handlerData(KafkaConsumer<String, String> consumer){
        boolean running = true;
        try {
             while(running) {
                 ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
                 for (TopicPartition partition : records.partitions()) {
                     List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                     for (ConsumerRecord<String, String> record : partitionRecords) {
                         System.out.println(record.offset() + ": " + record.value());
                     }
                     long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                     //注意提交的offset是程序将要读取的下一条消息的offset
                     consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
                 }
             }
         } finally {
           consumer.close();
         }
    }
    
    /**
     * 关闭消费者
     * @param consumer
     */
    public void closeConsumer(KafkaConsumer<String, String> consumer){
        if(consumer != null){
            consumer.close();
        }
    }
 
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值