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

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

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

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

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

[java]  view plain  copy
  1. <span style="font-size:14px;"//订阅指定的topic  
  2. consumer.subscribe(Arrays.asList(topic));</span>  
(2)还有一种消费数据的方式是可以通过assign函数指定要消费的分区数据,这种方式可以指定从分区的任意位置开始消费数据,当然这种
[java]  view plain  copy
  1. <span style="font-size:14px;">//消费者指定要消费的分区,指定分区之后消费者崩溃之后 不会引发分区reblance  
  2. consumer.assign(list);</span>  
消费数据的方式,如果消费者奔溃之后,不会引发分区reblance,也就是说assign的consumer不会拥有kafka的group management机制。

我们上面说过,同一个分片只能由消费者分组中的同一个消费者进行消费,假设当消费者A使用assign指定分区进行消费时,如果这时消费者A使用的分组group B,是通过subscribe订阅了这个主题的分片时,由于消费者A不加入group management,它相当于一个独立的临时消费者,这时消费者A也是可以正常消费的,看起来就是一个分片被一个消费者组中的多个消费者消费一样。


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

[java]  view plain  copy
  1. //一般配置earliest 或者latest 值  
  2. rops.put("auto.offset.reset""latest");  

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


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


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


 

[java]  view plain  copy
  1. /** 
  2.  *  
  3.  * @author yujie.wang 
  4.  * kafka生产者示例代码 
  5.  */  
  6. public class Producer_Sample {  
  7.     //kafka集群机器  
  8.     private static final String KAFKA_HOSTS = "10.4.30.151:9092,10.4.30.151:9093,10.4.30.151:9094";  
  9.     //topic名称  
  10.     private static final String TOPIC = "my-replicated-topic_2";  
  11.       
  12.     public static void main(String[] args) {  
  13.         // TODO Auto-generated method stub  
  14.         Producer_Sample producer = new Producer_Sample();  
  15.         producer.producer_send(TOPIC);  
  16.         System.out.println("end");  
  17.     }  
  18.       
  19.     /** 
  20.      * 生产者生产数据 
  21.      * 发送消息是异步进行,一旦消息被保存到分区缓存中,send方法就返回 
  22.      * 一旦消息被接收 就会调用callBack 
  23.      * @param topic 
  24.      */  
  25.     public void producer_send(String topic){  
  26.         Properties props = new Properties();  
  27.         //kafka集群机器  
  28.         props.put("bootstrap.servers", KAFKA_HOSTS);  
  29.         //生产者发送的数据需要等待主分片和其副本都保存才发回确认消息  
  30.         props.put("acks""all");  
  31.         //生产者发送失败后的确认消息  
  32.         props.put("retries"0);  
  33.         //生产者 每个分区缓存大小 16K  
  34.         props.put("batch.size"16384);  
  35.         //生产者发送分区缓存中数据前停留时间  
  36.         props.put("linger.ms"1);  
  37.         //生产者可用缓存总量大小 32M  
  38.         props.put("buffer.memory"33554432);  
  39.         props.put("key.serializer""org.apache.kafka.common.serialization.StringSerializer");  
  40.         props.put("value.serializer""org.apache.kafka.common.serialization.StringSerializer");  
  41.            
  42.         Producer<String, String> producer = new KafkaProducer<String,String>(props);  
  43.         for(int i = 220; i < 230; i++){  
  44.             //发送消息是异步进行,一旦消息被保存到分区缓存中,send方法就返回  
  45.             // producer.send(new ProducerRecord<String, String>("my-replicated-topic_1", Integer.toString(i), Integer.toString(i)));  
  46.             producer.send(new ProducerRecord<String, String>(topic, "call___"+Integer.toString(i+20), "call___"+Integer.toString(i)),  
  47.                     new Call());  
  48.             System.out.println("send return I: "+ i);  
  49.         }  
  50.   
  51.         producer.close();  
  52.     }  
  53.   
  54.     /** 
  55.      *消息被保存之后的回调方法 
  56.      */  
  57.     class Call implements Callback{  
  58.   
  59.         @Override  
  60.         public void onCompletion(RecordMetadata recordmetadata,  
  61.                 Exception exception) {  
  62.             // TODO Auto-generated method stub  
  63.             System.out.println("callBack: "+ recordmetadata.checksum() + " recordmetadata content : "+recordmetadata.toString());  
  64.         }  
  65.           
  66.     }  
  67. }  

[java]  view plain  copy
  1. /** 
  2.  * @author yujie.wang 
  3.  * kafka消费者示例,包含随机位置消费和最多一次消费方式 
  4.  * 消费者提交消费数据offset 分为自动提交和手动控制提交 
  5.  *  
  6.  * 这份代码示例中包含了 多种从kafka的任意位置获取数据的方式 
  7.  */  
  8. public class Consumer_Sample {  
  9.   
  10.     //kafka集群机器  
  11.     private static final String KAFKA_HOSTS = "10.4.30.151:9092,10.4.30.151:9093,10.4.30.151:9094";  
  12.     //topic名称  
  13.     private static final String TOPIC = "my-replicated-topic_2";  
  14.       
  15.     public static void main(String[] args) {  
  16.         // TODO Auto-generated method stub  
  17.         Consumer_Sample consumer = new Consumer_Sample();  
  18.         //从分区的末尾 或者已存在groupid的请情况下从未消费位置开始消费数据  
  19.         consumer.consumerSubscribe("true", TOPIC);  
  20.         // 通过实现ConsumerRebalanceListener接口 进而时间任意位置的消费  
  21.         consumer.consumerSubscribeImplListener("true", TOPIC);  
  22.         //从指定的分区  开始位置seekToBeginning 或者任意位置seek消费数据  
  23.         consumer.consumerAssin("true", TOPIC);  
  24.         //通过配置属性auto.offset.reset 来设置消费者从分区开头或者末尾进行消费,但是需要使用一定条件的group Id  
  25.         consumer.consumerAutoOffsetReset("true", TOPIC);  
  26.         System.out.println("consumer end");  
  27.     }  
  28.       
  29.       
  30.     /** 
  31.      * 直接通过订阅一个指定分区来消费数据 
  32.      * (1)如果该groupId消费者分组下 有消费者提交过offset,则从 当前提交的offset位置开始消费数据 
  33.      * (2)如果该groupId消费者分组下 没有有消费者提交过offset,则从 当前log添加的最后位置(也就是数据的末尾)开始消费数据 
  34.      * @param isAutoCommitBool 
  35.      * @param topic 
  36.      */  
  37.     public void consumerSubscribe(final String isAutoCommitBool, final String topic){  
  38.          Properties props = new Properties();  
  39.          //配置kafka集群机器  
  40.          props.put("bootstrap.servers", KAFKA_HOSTS);  
  41.          //消费者分组  
  42.          props.put("group.id""yujie37");  
  43.          //这里设置 消费者自动提交已消费消息的offset  
  44.          props.put("enable.auto.commit", isAutoCommitBool);  
  45.          // 设置自动提交的时间间隔为1000毫秒  
  46.          props.put("auto.commit.interval.ms""1000");  
  47.          // 设置每次poll的最大数据个数  
  48.          props.put("max.poll.records"5);  
  49.          props.put("key.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  50.          props.put("value.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  51.          final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
  52.          //订阅topic  
  53.          consumer.subscribe(Arrays.asList(topic));  
  54.          List<PartitionInfo> parList = consumer.partitionsFor(topic);  
  55.   
  56.          //打印出分区信息  
  57.          printPartition(parList);  
  58.   
  59.          //消费数据  
  60.          while (true) {  
  61.              ConsumerRecords<String, String> records = consumer.poll(5000);  
  62.              System.out.println("topic: "+topic + " pool return records size: "+ records.count());  
  63.              for (ConsumerRecord<String, String> record : records){  
  64.                  System.out.println(record.toString());  
  65.                   //手动提交已消费数据的offset  
  66.                  if("false".equalsIgnoreCase(isAutoCommitBool)){  
  67.                     consumer.commitSync();  
  68.                  }  
  69.                   
  70.              }  
  71.              
  72.          }  
  73.     }  
  74.       
  75.       
  76.     /** 
  77.      *  
  78.      * @param isAutoCommitBool true 开启自动提交offset;false 不开启 
  79.      * @param topic 
  80.      * (1)如果该groupId消费者分组下 有消费者提交过offset,则从 当前提交的offset位置开始消费数据 
  81.      * (2)如果该groupId消费者分组下 没有有消费者提交过offset,则从 当前log添加的最后位置(也就是数据的末尾)开始消费数据 
  82.      *  
  83.      * 注意如果enable.auto.commit 设置为false,如果消费完数据没有提交已消费数据的offset, 
  84.      * 则会出现重复消费数据的情况 
  85.      *  
  86.      * 通过实现ConsumerRebalanceListener接口中的onPartitionsAssigned方法,并在其中调用消费者的seek或者seekToBeginning 
  87.      * 方法定位分区的任意位置或者开头位置 
  88.      */  
  89.     public void consumerSubscribeImplListener(final String isAutoCommitBool, final String topic){  
  90.          Properties props = new Properties();  
  91.          //配置kafka集群机器  
  92.          props.put("bootstrap.servers", KAFKA_HOSTS);  
  93.          //消费者分组  
  94.          props.put("group.id""yujie26");  
  95.          //这里设置 消费者自动提交已消费消息的offset  
  96.          props.put("enable.auto.commit", isAutoCommitBool);  
  97.          // 设置自动提交的时间间隔为1000毫秒  
  98.          props.put("auto.commit.interval.ms""1000");  
  99.          // 设置每次poll的最大数据个数  
  100.          props.put("max.poll.records"5);  
  101.          props.put("key.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  102.          props.put("value.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  103.          final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
  104.          //订阅topic,并实现ConsumerRebalanceListener  
  105.          consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener(){  
  106.             @Override  
  107.             public void onPartitionsRevoked(//分区撤销时,消费者可以向该分区提交自己当前的offset  
  108.                     Collection<TopicPartition> collection) {  
  109.                 // TODO Auto-generated method stub  
  110.                 if("false".equalsIgnoreCase(isAutoCommitBool)){  
  111.                     //consumer.commitSync();  
  112.                 }  
  113.             }  
  114.   
  115.             @Override  
  116.             public void onPartitionsAssigned(//当分区分配给消费者时,消费者可以通过该方法重新定位需要消费的数据位置  
  117.                     Collection<TopicPartition> collection) {  
  118.                 // TODO Auto-generated method stub  
  119.                 //将消费者定位到各个分区的开始位置进行消费  
  120.         /*      consumer.seekToBeginning(collection); 
  121.                 System.out.println("seek beg");*/  
  122.               
  123.                 Iterator it = collection.iterator();  
  124.                 while(it.hasNext()){  
  125.                     //将消费者定位到指定分区的指定位置7进行消费  
  126.                     consumer.seek((TopicPartition)it.next(), 7);  
  127.                 }  
  128.                   
  129.             }  
  130.          });  
  131.          while (true) {  
  132.              ConsumerRecords<String, String> records = consumer.poll(5000);  
  133.              System.out.println("topic: "+topic + "pool return records size: "+ records.count());  
  134.              for (ConsumerRecord<String, String> record : records){  
  135.                  System.out.println(record.toString());  
  136.                   //手动提交已消费数据的offset  
  137.                  if("false".equalsIgnoreCase(isAutoCommitBool)){  
  138.                     consumer.commitSync();  
  139.                  }  
  140.                   
  141.              }  
  142.              
  143.          }  
  144.     }  
  145.       
  146.       
  147.     /** 
  148.      *  
  149.      * @param isAutoCommitBool true 开启自动提交offset;false 不开启 
  150.      * @param topic 
  151.      * 如果groupId之前存在 , 则从之前提交的最后消费数据的offset处继续开始消费数据 
  152.      * 如果groupId之前不存在,则从当前分区的最后位置开始消费 
  153.      *  
  154.      * 注意如果enable.auto.commit 设置为false,如果消费完数据没有提交已消费数据的offset, 
  155.      * 则会出现重复消费数据的情况 
  156.      */  
  157.     public void consumerAutoOffsetReset(final String isAutoCommitBool, final String topic){  
  158.          Properties props = new Properties();  
  159.          //配置kafka集群机器  
  160.          props.put("bootstrap.servers", KAFKA_HOSTS);  
  161.          //消费者分组  
  162.          props.put("group.id""yujie32");  
  163.          //这里设置 消费者自动提交已消费消息的offset  
  164.          props.put("enable.auto.commit", isAutoCommitBool);  
  165.          // 设置自动提交的时间间隔为1000毫秒  
  166.          props.put("auto.commit.interval.ms""1000");  
  167.          // 设置每次poll的最大数据个数  
  168.          props.put("max.poll.records"5);  
  169.          //设置使用最开始的offset偏移量为该group.id的最早。如果不设置,则会是latest即该topic最新一个消息的offset  
  170.          //如果采用latest,消费者只能得道其启动后,生产者生产的消息  
  171.          //一般配置earliest 或者latest 值  
  172.          props.put("auto.offset.reset""latest");  
  173.          props.put("key.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  174.          props.put("value.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  175.          final KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
  176.          //订阅topic,并实现ConsumerRebalanceListener  
  177.          consumer.subscribe(Arrays.asList(topic));  
  178.          while (true) {  
  179.              ConsumerRecords<String, String> records = consumer.poll(5000);  
  180.              System.out.println("topic: "+topic + "pool return records size: "+ records.count());  
  181.              for (ConsumerRecord<String, String> record : records){  
  182.                  System.out.println(record.toString());  
  183.                   //手动提交已消费数据的offset  
  184.                  if("false".equalsIgnoreCase(isAutoCommitBool)){  
  185.                     consumer.commitSync();  
  186.                  }  
  187.                   
  188.              }  
  189.              
  190.          }  
  191.     }  
  192.   
  193.       
  194.     /** 
  195.      * 通过assign分配的分区,消费者发生故障 Server端不会触发分区重平衡(即使该消费者共享某个已有的groupId),每个消费者都是独立工作的 
  196.      * 为了避免offset提交冲突,需要确保每个消费者都有唯一的groupId 
  197.      * 从指定的分区的开头开始消费数据 
  198.      * @param isAutoCommitBool true 开启自动提交offset;false 不开启 
  199.      * @param topic 
  200.      */  
  201.     public void consumerAssin(String isAutoCommitBool,String topic){  
  202.          Properties props = new Properties();  
  203.          //配置kafka集群机器  
  204.          props.put("bootstrap.servers", KAFKA_HOSTS);  
  205.          //消费者分组  
  206.          props.put("group.id""yujie35");  
  207.          //这里设置 消费者自动提交已消费消息的offset  
  208.          props.put("enable.auto.commit", isAutoCommitBool);  
  209.          // 设置自动提交的时间间隔为1000毫秒  
  210.          props.put("auto.commit.interval.ms""1000");  
  211.          // 设置每次poll的最大数据个数  
  212.          props.put("max.poll.records"5);  
  213.          props.put("key.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  214.          props.put("value.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  215.          KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
  216.          //获得topic的所有分区  
  217.          List<PartitionInfo> parList = consumer.partitionsFor(topic);  
  218.          //打印出分区信息  
  219.          printPartition(parList);  
  220.            
  221.          List<TopicPartition> list = new ArrayList<TopicPartition>();  
  222.          for(PartitionInfo par : parList){  
  223.              TopicPartition partition = new TopicPartition(topic, par.partition());  
  224.              list.add(partition);  
  225.          }  
  226.          //消费者指定要消费的分区,指定分区之后消费者崩溃之后 不会引发分区reblance  
  227.          consumer.assign(list);  
  228.   
  229.          //从list中所有分区的开头开始消费数据,这个操作不改变已提交的消费数据的offset  
  230.         // consumer.seekToBeginning(list);  
  231.        
  232.   
  233.      /*    for(TopicPartition tpar:list ){ 
  234.              //consumer.seek(tpar, position); 
  235.          } */  
  236.        
  237.   
  238.          while (true) {  
  239.              ConsumerRecords<String, String> records = consumer.poll(5000);  
  240.              System.out.println("topic: "+topic + " pool return records size: "+ records.count());  
  241.              for (ConsumerRecord<String, String> record : records){  
  242.                  System.out.println(record.toString());  
  243.                   //手动提交已消费数据的offset  
  244.                  if("false".equalsIgnoreCase(isAutoCommitBool)){  
  245.                     consumer.commitSync();  
  246.                  }  
  247.                   
  248.              }  
  249.              
  250.          }  
  251.     }  
  252.       
  253.   
  254.       
  255.       
  256.   
  257.       
  258.     public void printPartition(List<PartitionInfo> parList){  
  259.         for(PartitionInfo p : parList){  
  260.             System.out.println(p.toString());  
  261.         }  
  262.     }  
  263.       
  264.     /** 
  265.      * 单独处理每个分区中的数据,处理完了之后异步提交offset,注意提交的offset是程序将要读取的下一条消息的offset 
  266.      * @param consumer 
  267.      */  
  268.     public void handlerData(KafkaConsumer<String, String> consumer){  
  269.         boolean running = true;  
  270.         try {  
  271.              while(running) {  
  272.                  ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);  
  273.                  for (TopicPartition partition : records.partitions()) {  
  274.                      List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);  
  275.                      for (ConsumerRecord<String, String> record : partitionRecords) {  
  276.                          System.out.println(record.offset() + ": " + record.value());  
  277.                      }  
  278.                      long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();  
  279.                      //注意提交的offset是程序将要读取的下一条消息的offset  
  280.                      consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));  
  281.                  }  
  282.              }  
  283.          } finally {  
  284.            consumer.close();  
  285.          }  
  286.     }  
  287.       
  288.     /** 
  289.      * 关闭消费者 
  290.      * @param consumer 
  291.      */  
  292.     public void closeConsumer(KafkaConsumer<String, String> consumer){  
  293.         if(consumer != null){  
  294.             consumer.close();  
  295.         }  
  296.     }  
  297.   
  298. }  
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值