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条的数据,在后面的分批里一直都消费不到

 

亲测

个人总结, 设置批量消费后, 不要设定条件不提交,如果数据正确都应该提交,抛出异常 才能不提交消费数据,这样保证数据下次还能消费‘

 

 

 

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