深入浅出系列之 -- kafka消费者的三种语义

    本文主要详解kafka client的使用,包括kafka消费者的三种消费语义at-most-once,at-least-once,和exact-once message,生产者的使用等。

 

创建主题

bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic normal --partitions 2 --rerelication-factor 1

 

生产者

   private static Producer < String,String > createProducer(){ 
       Properties props = new Properties(); 
       props.put(“bootstrap.servers”,“localhost:9092”); 
       props.put(“acks”,“all”); 
       props.put(“retries”,0); 
       //控制发送者在发布到Kafka之前等待批处理的字节数。
       props.put(“batch.size”,10); 
       props.put(“linger.ms”,); 
       props.put(“key.serializer”,“org.apache.kafka.common.serialization.StringSerializer”); 
       props.put(“value.serializer”,“org.apache.kafka.common.serialization.StringSerializer”); 
       //返回新的KafkaProducer(道具); 
   } 

 

消费者

消费者注册到卡夫卡有多种方式:

订阅:这种方式在新增的话题或者分区或者消费者增加或者消费者减少的时候,会进行消费者组内消费者的再平衡。

分配:这种方式注册的消费者不会进行重新平衡。

上面两种方式都是可以实现,三种消费语义的。具体API的使用请看下文。

1.最多一次kafka消费者

最多一次消费语义是kafka消费者的默认实现。配置这种消费者最简单的方式是

1)enable.auto.commit设置为真。

2)auto.commit.interval.ms设置为一个较低的时间范围。

3)consumer.commitSync()不要调用该方法。

由于上面的配置,就可以使得kafka有线程负责按照指定间隔提交偏移。但是这种方式会使得kafka消费者有两种消费语义:

消费语义最多一次 :

    消费者的偏移已经提交,但是消息还在处理,这个时候挂了,再重启的时候会从上次提交的偏移处消费,导致上次在处理的消息部分丢失

消费语义最少一次:

    消费者已经处理完了,但是偏移还没提交,那么这个时候消费者挂了,就会导致消费者重复消费消息处理。但是由于auto.commit.interval.ms设置为一个较低的时间范围,会降低这种情况出现的概率

代码如下:

public class AtMostOnceConsumer { 
       public static void main(String [] str)throws InterruptedException { 
           System.out.println(“Starting AtMostOnceConsumer ...”); 
           执行(); 
       } 
       私人 静态 无效执行()抛出InterruptedException的{ 
               KafkaConsumer < 字符串,字符串 >消费者= createConsumer(); 
               //订阅该主题中的所有分区。'assign'可以在这里使用
               //而不是'subscribe'来订阅特定的分区。
               consumer.subscribe(Arrays.asList(“正常话题”)); 
               processRecords(消费者); 
       } 
       私人 静态 KafkaConsumer < 字符串,字符串 > createConsumer(){ 
               属性道具= 新属性(); 
               props.put(“bootstrap.servers”,“localhost:9092”); 
               String consumeGroup = “cg1” ; 
               props.put(“group.id”,consumeGroup); 
               //如果发生自动提交,请设置此属性。
               props.put(“enable.auto.commit”,“
               //自动提交间隔,kafka将在此间隔提交偏移量。
               props.put(“auto.commit.interval.ms”,“101”); 
               //这是如何控制每个轮询
               props.put中读取的记录数(“max.partition.fetch.bytes”,“135”); 
               //如果你想从头开始阅读,请设置此项。
               // props.put(“auto.offset.reset”,“earliest”); 
               props.put(“heartbeat.interval.ms”,“3000”); 
               props.put(“session.timeout.ms”,“6001”);
,
                       “org.apache.kafka.common.serialization.StringDeserializer”); 
               props.put(“value.deserializer”,
                       “org.apache.kafka.common.serialization.StringDeserializer”); 
               返回 新的 KafkaConsumer < String,String >(props); 
       } 
       私人 静态 无效 processRecords(KafkaConsumer < 字符串,字符串 >消费者){ 
               而(真){ 
                       ConsumerRecords < 字符串,字符串 >记录= consumer.poll(100);
                       long lastOffset = 0 ; 
                       for(ConsumerRecord < String,String > record:records){ 
                               System.out.printf(“\ n \ roffset =%d,key =%s,value =%s”,record.offset(),record.key() ,record.value()); 
                               lastOffset = record.offset(); 
                        } 
               的System.out.println(“lastOffset如下:” + lastOffset); 
               处理(); 
               } 
       } 
       私人 静态 无效过程()抛出InterruptedException的{
               //创建一些延迟来模拟消息的处理。
               Thread.sleep(20); 
       } 
}

2.至少一次kafka消费者

实现最少一次消费语义的消费者也很简单。

1)设置enable.auto.commit为假

2)消息处理完之后手动调用consumer.commitSync()

这种方式就是要手动在处理完该次轮询得到消息之后,调用偏移异步提交函数consumer.commitSync()。建议是消费者内部实现密等,来避免消费者重复处理消息进而得到重复结果。最多一次发生的场景是消费者的消息处理完并输出到结果库(也可能是部分处理完),但是偏移还没提交,这个时候消费者挂掉了,再重启的时候会重新消费并处理消息。

代码如下:

public class AtLeastOnceConsumer { 
   public static void main(String [] str)throws InterruptedException { 
           System.out.println(“Starting AutoOffsetGuranteedAtLeastOnceConsumer ...”); 
           执行(); 
    } 
   私人 静态 无效执行()抛出InterruptedException的{ 
           KafkaConsumer < 字符串,字符串 >消费者= createConsumer(); 
           //订阅该主题中的所有分区。'assign'可以在这里使用
           //而不是'subscribe'来订阅特定的分区。
           consumer.subscribe(Arrays.asList(“正常话题”)); 
           processRecords(消费者); 
    } 
    私人 静态 KafkaConsumer < 字符串,字符串 > createConsumer(){ 
           属性道具= 新属性(); 
           props.put(“bootstrap.servers”,“localhost:9092”); 
           String consumeGroup = “cg1” ; 
           props.put(“group.id”,consumeGroup); 
           //如果发生自动提交,请设置此属性。
           props.put(“enable.auto.commit”,“true”);
           //使自动提交间隔为大数,以便不会发生自动提交,
           //我们将通过consumer.commitSync()控制偏移提交; 处理完//消息后。
           props.put(“auto.commit.interval.ms”,“999999999999”); 
           //这是如何控制每个轮询
           props.put中读取的消息数量(“max.partition.fetch.bytes”,“135”); 
           props.put(“heartbeat.interval.ms”,“3000”); 
           props.put(“session.timeout.ms”,“6001”);
,“org.apache.kafka.common.serialization.StringDeserializer”); 
           props.put(“value.deserializer”,“org.apache.kafka.common.serialization.StringDeserializer”); 
           返回 新的 KafkaConsumer < String,String >(props); 
   } 
    私人 静态 无效 processRecords(KafkaConsumer < 字符串,字符串 >消费者)抛出{ 
           而(真){ 
                   ConsumerRecords < 字符串,字符串 >记录= consumer.poll(100);
                   long lastOffset = 0 ; 
                   for(ConsumerRecord < String,String > record:records){ 
                       System.out.printf(“\ n \ roffset =%d,key =%s,value =%s”,record.offset(),record.key() ,record.value()); 
                       lastOffset = record.offset(); 
                   } 
                   的System.out.println(“lastOffset如下:” + lastOffset); 
                   处理(); 
                   //以下调用对于控制偏移提交很重要。在//完成业务流程处理后执行此调用
                   。
                   consumer.commitSync(); 
           } 
   } 
   私人 静态 无效过程()抛出InterruptedException的{ 
       //创建一些延迟,以模拟记录的处理。
       Thread.sleep(20); 
   } 
}

3.使用subscribe实现Exactly-once 

使用subscribe实现Exactly-once很简单,具体思路如下:

1)将enable.auto.commit设置为假。

2)不调用consumer.commitSync()。

3)使用SUBCRIBE定于话题。

4)实现一个ConsumerRebalanceListener,在该监听器内部执行consumer.seek(topicPartition,偏移),从指定的主题/分区的偏移处启动。

5)在处理消息的时候,要同时控制保存住每个消息的偏移量。以原子事务的方式保存偏移和处理的消息结果。传统数据库实现原子事务比较简单。但对于非传统数据库,比如HDFS或者nosql的,为了实现这个目标,只能将偏移与消息保存在同一行。

6)实现密等,作为保护层。

代码如下:

public class ExactlyOnceDynamicConsumer { 
      private static OffsetManager offsetManager = new OffsetManager(“storage2”); 
       public static void main(String [] str)throws InterruptedException { 
               System.out.println(“Starting ExactlyOnceDynamicConsumer ...”); 
               readMessages(); 
       } 
       私人 静态 无效 readMessages()抛出InterruptedException的{ 
               KafkaConsumer < 字符串,字符串 >消费者= createConsumer();
               //手动控制偏移量,但将消费者注册到主题以动态获取
               //分配的分区。在MyConsumerRebalancerListener内部使用
               // consumer.seek(topicPartition,offset)来控制要读取的消息的偏移量。
               consumer.subscribe(Arrays.asList(“normal-topic”),
                               new MyConsumerRebalancerListener(consumer)); 
               processRecords(消费者); 
       } 
       私人 静态 KafkaConsumer < 字符串,字符串 > createConsumer(){ 
               属性道具= 新属性(); 
               props.put(“bootstrap.servers”,“localhost:9092”); 
               String consumeGroup = “cg3” ; 
               props.put(“group.id”,consumeGroup); 
               //以下是关闭自动提交的关键设置。
               props.put(“enable.auto.commit”,“false”); 
               props.put(“heartbeat.interval.ms”,“2000”); 
               props.put(“session.timeout.ms”,“6001”); 
               //控制每次投票的最大数据,
“max.partition.fetch.bytes”,“140”); 
               props.put(“key.deserializer”,                                 “org.apache.kafka.common.serialization.StringDeserializer”); 
               props.put(“value.deserializer”,                         “org.apache.kafka.common.serialization.StringDeserializer”); 
               返回 新的 KafkaConsumer < String,String >(props); 
       } 
       私人 静态 无效 processRecords(KafkaConsumer < 字符串,字符串 >
           true){ 
                   ConsumerRecords < String,String > records = consumer.poll(100); 
                   for(ConsumerRecord < String,String > record:records){ 
                           System.out.printf(“offset =%d,key =%s,value =%s \ n”,record.offset(),record.key(), record.value()); 
                           //在外部存储中保存已处理的偏移量 
                           offsetManager.saveOffsetInExternalStore(record.topic(),record.partition(),record.offset()); 
                   } 
              }
       } 
} 
公共 类 MyConsumerRebalancerListener 实现                                 org.apache.kafka.clients.consumer.ConsumerRebalanceListener { 
       私人 OffsetManager offsetManager = 新 OffsetManager( “ 存储2”); 
       private Consumer < String,String > consumer; 
       public MyConsumerRebalancerListener(Consumer < String,String > consumer){ 
               this .consumer = consumer; 
       } 
       公共 空隙 onPartitionsRevoked(集合<TopicPartition>分区){
               for(TopicPartition partition:partitions){ 
                   offsetManager.saveOffsetInExternalStore(partition.topic(),partition.partition(),consumer.position(partition)); 
               } 
       } 
       公共 空隙 onPartitionsAssigned(集合<TopicPartition>分区){ 
               对于(TopicPartition分区:分区){ 
                       consumer.seek(分区,offsetManager.readOffsetFromExternalStore(partition.topic(),partition.partition())); 
               } 
       } 
} 
/ **
*分区偏移量存储在外部存储器中。在这种情况下,在
*程序运行的本地文件系统中。
* / 
public class OffsetManager { 
       private String storagePrefix; 
       public OffsetManager(String storagePrefix){ 
               this .storagePrefix = storagePrefix; 
       } 
   / ** 
       *覆盖外部存储中主题的偏移量。
       * 
       * @param主题 - 主题名称。
       * @param partition - 主题的分区。
       * @param offset - 要存储的偏移量。
       * / 
       void saveOffsetInExternalStore(Stringtopic,int partition,long offset){ 
           try { 
               FileWriter writer = new FileWriter(storageName(topic,partition),false); 
               BufferedWriter bufferedWriter = new BufferedWriter(writer); 
               bufferedWriter.write(offset + “”); 
               bufferedWriter.flush(); 
               bufferedWriter.close(); 
           } catch(Exception e){ 
                   e.printStackTrace(); 
                   抛出 新的 RuntimeException(e); 
           } 
       } 
       / **
           * @return他提供的主题和分区的最后偏移量+ 1。
       * /
       long readOffsetFromExternalStore(String topic,int partition){ 
               try { 
                       Stream < String > stream = Files.lines(Paths.get(storageName(topic,partition))); 
                       return Long.parseLong(stream.collect(Collectors.toList())。get(0))+ 1 ; 
               } catch(Exception e){ 
                   e.printStackTrace(); 
               } 
               return 0 ; 
       } 
       私人 字符串 storageName(字符串topic,int partition){ 
           return storagePrefix + “ - ” + topic + “ - ” + partition; 
       } 
}

4.使用指定实现完全一次

使用assign实现Exactly-once也很简单,具体思路如下:

1)将enable.auto.commit设置为假。

2)不调用consumer.commitSync()。

3)调用指定注册卡夫卡消费者到卡夫卡

4)初次启动的时候,调用consumer.seek(topicPartition,偏移)来指定偏移量。

5)在处理消息的时候,要同时控制保存住每个消息的偏移量。以原子事务的方式保存偏移和处理的消息结果。传统数据库实现原子事务比较简单。但对于非传统数据库,比如HDFS或者nosql的,为了实现这个目标,只能将偏移与消息保存在同一行。

6)实现密等,作为保护层。

代码如下:

public class ExactlyOnceStaticConsumer { 
       private static OffsetManager offsetManager = new OffsetManager(“storage1”); 
       public static void main(String [] str)抛出InterruptedException,IOException { 
               System.out.println(“Starting ExactlyOnceStaticConsumer ...”); 
               readMessages(); 
       } 
       私人 静态 无效 readMessages()抛出InterruptedException的,IOException异常{ 
               KafkaConsumer < 字符串,字符串 >消费者= createConsumer();
               String topic = “normal-topic” ; 
               int partition = 1 ; 
               TopicPartition topicPartition = 
                               registerConsumerToSpecificPartition(consumer,topic,partition); 
               //从外部存储中读取主题和分区的偏移量。
               long offset = offsetManager.readOffsetFromExternalStore(topic,partition); 
               //使用搜索并转到该主题和分区的精确偏移量。
               consumer.seek(topicPartition,offset); 
               processRecords(消费者); 
       } 
       私人 静态 KafkaConsumer < 字符串,字符串> createConsumer(){ 
               Properties props = new Properties(); 
               props.put(“bootstrap.servers”,“localhost:9092”); 
               String consumeGroup = “cg2” ; 
               props.put(“group.id”,consumeGroup); 
               //以下是关闭自动提交的关键设置。
               props.put(“enable.auto.commit”,“false”); 
               props.put(“heartbeat.interval.ms”,“2000”); 
               props.put(“session.timeout.ms”
               //控制每个轮询的最大数据,确保此值大于最大//单个消息大小
               props.put(“max.partition.fetch.bytes”,“140”); 
               props.put(“key.deserializer”,                                     “org.apache.kafka.common.serialization.StringDeserializer”); 
               props.put(“value.deserializer”,                                     “org.apache.kafka.common.serialization.StringDeserializer”); 
               返回 新的 KafkaConsumer < String,String >(props);

       
           *手动侦听特定主题分区。但是,如果您正在寻找如何*动态侦听分区并希望手动控制偏移量的示例,请参阅
           * ExactlyOnceDynamicConsumer.java 
           * / 
        private static TopicPartition registerConsumerToSpecificPartition(
                   KafkaConsumer < String,String > consumer,String topic,int partition){ 
                   TopicPartition topicPartition = new TopicPartition(topic,partition); 
                   List <TopicPartition> partitions = Arrays.asList(topicPartition);
                   consumer.assign(分区); 
                   return topicPartition; 
         } 
           / ** 
               *在外部存储中处理数据和存储偏移量。最佳做法是以
               原子方式执行这些操作。
               * / 
           private static void processRecords(KafkaConsumer < String,String > consumer)抛出{ 
                   while(true){ 
                          ConsumerRecords < String,String > records = consumer.poll(100); 
                           for(ConsumerRecord < String,字符串 >记录:记录){ 
                                   System.out.printf(“offset =%d,key =%s,value =%s \ n”,record.offset(),record.key(),record.value()) ; 
                                   offsetManager.saveOffsetInExternalStore(record.topic(),record.partition(),record.offset()); 
                           } 
                   } 
           } 
}

 

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

      考虑一千次,不如去做一次;犹豫一万次,不如实践一次;华丽的跌倒,胜过无谓的彷徨,将来的你,一定会感谢现在奋斗的你。欢迎大家加入大数据交流群:725967421     一起交流,一起进步!!

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------- 
 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章