深入淺出系列之 -- 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     一起交流,一起進步!!

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

 

 

 

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