kafka不同組消費同一主題topic生產者配置

因數涉及到數據的原子性,必須多個分組消費同一消費主題,寫入數據庫後, 可以自行回回滾數據,重新消費,不影響其它數據消費的目的,實現分批次拉取數據等,也是走了很多坑

第一, 是環境兼容性問題,高版本可能更容易實現多分組消費同一主題topic 

依賴環境 springboot15.0+spring-kafka-1.1.1.RELEASE

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.0.RELEASE</version>
    <!--  <version>1.5.0.RELEASE</version>-->
</parent>
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <!-- <version>1.3.5.RELEASE</version>-->
    <version>1.1.1.RELEASE</version>
</dependency>

最開始是springboot1.5.6,有些包衝突後, 降低了依賴

第二 配置文件  批量消費的工廠類

 特別注意的地方是, 配置group-id這幾個常量方法,在工廠裏面配置後,在消費端只需要配置ID不一樣就可以

package com.zenlayer.ad.kafuka;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.listener.AbstractMessageListenerContainer;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
@EnableKafka
public class KafkaConfiguration {
    /**
     * @author lmc
     * @version 2019/12/06 下午04:07
     */
    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Value("${spring.kafka.consumer.enable-auto-commit}")
    private Boolean autoCommit;

    @Value("${spring.kafka.consumer.auto-commit-interval}")
    private Integer autoCommitInterval;

    @Value("${spring.kafka.consumer.group-id}")
    private String groupId;

    @Value("${spring.kafka.consumer.group-flow-id}")
    private String groupFlowId;

    @Value("${spring.kafka.consumer.group-flowgroup-id}")
    private String groupFlowGroupId;

    @Value("${spring.kafka.consumer.max-poll-records}")
    private Integer maxPollRecords;

    @Value("${spring.kafka.consumer.auto-offset-reset}")
    private String autoOffsetReset;

    @Value("${spring.kafka.producer.retries}")
    private Integer retries;

    @Value("${spring.kafka.producer.batch-size}")
    private Integer batchSize;

    @Value("${spring.kafka.producer.buffer-memory}")
    private Integer bufferMemory;

    /**
     * 生產者配置信息
     */
    @Bean
    public Map<String, Object> producerConfigs() {
        Map<String, Object> props = new HashMap<String, Object>();
        props.put(ProducerConfig.ACKS_CONFIG, "0");
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ProducerConfig.RETRIES_CONFIG, retries);
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return props;
    }

    /**
     * 生產者工廠
     */
    @Bean
    public ProducerFactory<String, String> producerFactory() {
        return new DefaultKafkaProducerFactory<>(producerConfigs());
    }

    /**
     * 生產者模板
     */
    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }

     /**
     * 消費者配置信息   單個配置組  可以使用bean注入,多個配置需要分開
     */
    //  @Bean
    public Map<String, Object> consumerConfigs(String groupsid) {
        Map<String, Object> props = new HashMap<String, Object>();
        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupsid);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, autoCommit);// 手動提交 配置 false
     //   props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval);//這是自動提交的間隔時間 手動提交此值可以不設置 
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 120000);
        props.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, 180000);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        return props;
    }

    /**
     * 消費者批量工程
     */
    @Bean
    public KafkaListenerContainerFactory<?> batchFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs(groupId)));
        // 設置爲批量消費,每個批次數量在Kafka配置參數中設置ConsumerConfig.MAX_POLL_RECORDS_CONFIG
        factory.setBatchListener(true);
        factory.setConcurrency(4);
        factory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.MANUAL_IMMEDIATE);
        factory.getContainerProperties().setPollTimeout(30000);
        return factory;
    }

    /**
     * 消費者批量工程 帶寬組批量消費工廠
     */
    @Bean
    public KafkaListenerContainerFactory<?> batchFlowFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();

        factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs(groupFlowId)));
        // 設置爲批量消費,每個批次數量在Kafka配置參數中設置ConsumerConfig.MAX_POLL_RECORDS_CONFIG
        factory.setBatchListener(true);
        factory.setConcurrency(3);
        factory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.MANUAL_IMMEDIATE);
        factory.getContainerProperties().setPollTimeout(30000);
        return factory;
    }

    /**
     * 消費者批量工程 帶寬組集合,客戶流量彙總批量消費工廠
     */
    @Bean
    public KafkaListenerContainerFactory<?> batchFlowGroupFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs(groupFlowGroupId)));
        // 設置爲批量消費,每個批次數量在Kafka配置參數中設置ConsumerConfig.MAX_POLL_RECORDS_CONFIG
        factory.setBatchListener(true);
        factory.setConcurrency(3);
        factory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.MANUAL_IMMEDIATE);
        factory.getContainerProperties().setPollTimeout(30000);
        return factory;
    }


    /* **
     * kafka監聽工廠
     *
     * @param configurer
     * @return
     */

    public static KafkaConsumer getKafkaConsumer(String group) {
        Properties propstask = new Properties();
      /*  propstask.put("bootstrap.servers", "107.186.186.186:9092 107.186.186.187:9092 107.186.186.188:9092");
        //每個消費者分配獨立的組號
        propstask.put("group.id", group);
        //如果value合法,則自動提交偏移量
        propstask.put("enable.auto.commit", "false");
        // 每次拉取5000條
        propstask.put("max.poll.records", 10000);
        //設置多久一次更新被消費消息的偏移量
        propstask.put("auto.commit.interval.ms", "1000");
        //設置會話響應的時間,超過這個時間kafka可以選擇放棄消費或者消費下一條消息
        propstask.put("session.timeout.ms", "30000");
        //自動重置offset
        propstask.put("auto.offset.reset", "earliest");//latest  earliest
        propstask.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        propstask.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");*/
        return new KafkaConsumer<String, String>(propstask);
    }

}

第三, 消費端代碼如下  

特別注意點,這裏代版本只需要寫不同的ID 就行 不需要指定group這步已經在工廠配置時已經指定,有些高版本只需要對應的groupId="xxx" ,如果是新的ID,消費數據都是從最開始處消費,也就是第一條開始,包含歷史數據,

如果要從最新的數據消費,只能換個group-id分組名就OK,如果不換組名只換監聽ID是沒用的

@KafkaListener(id = "devfive", topics = "topic_collect_data", containerFactory = "batchFactory")

 

/**
     * 監聽topic_collect,批量消費  分區表綜合數據
     * Acknowledgment ack
     */
    @KafkaListener(id = "devfive", topics = "topic_collect_data", containerFactory = "batchFactory")
    public void listen100(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
        System.out.println(records.size() + "條數據被消費 原始數據組ID");

        try {
            JSONArray jsonarray = new JSONArray();
            for (ConsumerRecord<String, String> record : records) {
                String value = record.value();
                System.out.println(value);
                jsonarray.add(JSONObject.parse(value));
            }
            if (jsonarray.size() > 0) {
                String str = jsonarray.getJSONObject(0).getString("timestamp");
                if (str.length() <= 10) {
                    return;
                }
            }
            //  KafuKfaUtils kafuKfaUtils = new KafuKfaUtils();
            //  kafuKfaUtils.insertdata(jsonarray);
            ack.acknowledge();
            System.out.println("ack.acknowledge()  提交操作");
        } catch (Exception ex) {
            logger.error("原始表和分區表消費數據出錯 ", ex.getStackTrace());
        }
    }

    /**
     * 監聽topic_collect,批量消費 , 彙總帶組數據 
     */
    @KafkaListener(id = "flow", topics = "topic_collect_data", containerFactory = "batchFlowFactory")
    public void listenFlow(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
        System.out.println(records.size() + "條數據被消費 flow");
        try {
            JSONArray jsonarray = new JSONArray();
            for (ConsumerRecord<String, String> record : records) {
                String value = record.value();
                jsonarray.add(JSONObject.parse(value));
            }
            if (jsonarray.size() > 0) {
                String str = jsonarray.getJSONObject(0).getString("timestamp");
                if (str.length() <= 10) {
                    return;
                }
            }
            KafuKfaUtils kafuKfaUtils = new KafuKfaUtils();
            Map<String, Map<String, Integer>> map = consumerService.getclientflowportlist();
            if (map != null && map.size() > 0) {
                //    kafuKfaUtils.insertFlowdata(jsonarray, map);
                ack.acknowledge();
            } else {
                logger.info("帶寬組沒有數據 不提交  NO ACK");
                System.out.println("帶寬組沒有數據 不提交  NO ACK");
                return;
            }
        } catch (Exception ex) {
            logger.error("消寬組消費數據出錯 ", ex.getStackTrace());
        }
    }

    /**
     * 監聽topic_collect,批量消費 帶寬組集合數據消費
     */
    @KafkaListener(id = "flowsgp", topics = "topic_collect_data", containerFactory = "batchFlowGroupFactory")
    public void listenFlowGpTask(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
        System.out.println(records.size() + "條數據被消費 groupFlow");
        try {
            JSONArray jsonarray = new JSONArray();
            for (ConsumerRecord<String, String> record : records) {
                String value = record.value();
                jsonarray.add(JSONObject.parse(value));
            }
            if (jsonarray.size() > 0) {
                String str = jsonarray.getJSONObject(0).getString("timestamp");
                if (str.length() <= 10) {
                    return;
                }
            }
            KafuKfaUtils kafuKfaUtils = new KafuKfaUtils();
            Map<String, Map<String, Integer>> map_group = consumerService.getflowgroupportlist();
            if (map_group != null && map_group.size() > 0) {
                //  kafuKfaUtils.insertFlowGroupdata(jsonarray, map_group);
                ack.acknowledge();
            } else {
                logger.info("帶寬組集合沒有數據 不提交  NO ACK");
                System.out.println("帶寬組集合沒有數據 不提交  NO ACK");
                return;
            }
        } catch (Exception ex) {
            logger.error("寬組集合消費數據出錯 ", ex.getStackTrace());
        }
    }

結論  從最開始定時任務拉取數據,到現在的分批次分組拉取任務,多個踩坑,定時任務擔心任務處理不過來的時候定時時間到了,這樣會造成數據堆積,無法正常ACK,因爲彙總的話,數據量稍微大些較好,但又不能太小,

所以, 還是用批量消費 然後手動提交,少於多少條數據就不提交ACK,下次批量達到1000以上,才消費數據。

從kafka-stream 到定時任務拉取消費 再到批量消費數據,真是各種坑。

 

設置不提交條件 進行測試 原始數據組ID:這個沒有設置提交條件 看groupFlow和flow 輸出

 

System.out.println("拉取 " + records.size() + "條數據 flow");
if (records.size() < 10) {
    System.out.println("batchFlow 小於10條不提交  NO ACK");
    return;
}else {
    //todo 處理業務代碼,如果業務有問題也可以設置不提交offset
    ack.acknowledge();
}

小於 10條數據輸出結果

 

拉取 8條數據 groupFlow
batchFlowGroup 小於10條不提交  NO ACK
拉取 8條數據 flow
batchFlow 小於10條不提交  NO ACK
9條數據被消費 原始數據組ID
ack.acknowledge()  提交操作

拉取大於10條後,

 

拉取 10條數據 flow
拉取 10條數據 groupFlow
10條數據被消費 原始數據組ID
ack.acknowledge()  提交操作

 

親身測試的數據,基幹上面的版本,有如果有問題大家一直討論下;

注意:

巨坑, 按批次拉取數據消費的時候,只按照提交的批次來拉,如果再對拉取條件限制,將無法消費批次內 未全部拉出來的數據再消費 ,

比如, 我批次提交10條, 設置條件大於10條才手動提交,配置手動條件拉取的最大數是5000,後面發現只要大於10條數據,未提交,後面都無法自動消費 只有重啓服務纔可以消費掉所有數據。

把條件改爲大於15條數據,按常規,應該是第一次無法消費 然後第二次就會把第一次合併一起拉取, 事實上並不是,按批次拉取,不然無法提交,也無法消費數據

 

上圖顯示的, 小於10條的數據,在後面的分批裏一直都消費不到

 

親測

個人總結, 設置批量消費後, 不要設定條件不提交,如果數據正確都應該提交,拋出異常 才能不提交消費數據,這樣保證數據下次還能消費‘

 

 

 

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