消息中间件-kafka实战-第三章-SpringBoot集成kafka

参考

https://blog.youkuaiyun.com/weixin_70730532/article/details/125425798

一、消息模型及消息顺序

下图展示了消费者组、Kafka集群的消费模型

在这里插入图片描述

  • 分区是最小的并行单位
  • 一个消费者可以消费多个分区
  • 一个分区可以被多个消费者组里的消费者消息
  • 但是一个分区不能同时被同一个消费者组里的多个消费者消费

1、发布-订阅模式

每个消费者都属于不同的消费者组
在这里插入图片描述

2、点对点(一对一)

所有消费者都属于同一个消费者组
在这里插入图片描述

3、分区与消费顺序

在这里插入图片描述

  • 同一个生产者发送到同一个分区的消息,先发送的offset比后发送的offset小
  • 同一个生产者发送到不同分区的消息,消息顺序无法保证
  • 消费者按照消息在分区里的存放顺序进行消费的
  • Kafka只保证分区内的消费顺序,不能保证分区间的消费顺序

如果想保证消息的顺序,有如下两个办法

  • 这是一个分区,这样就可以保证所有的消息的顺序,但是失去了扩展性和性能
  • 支持通过设置消息的key,相同key的消息会发送到同一个分区

二、消息传递语义

最多一次-------消息可能会丢失,永远不重复发送
最少一次-------消息不会丢失,但是可能会重复
精确一次-------保证消息被传递到服务端且在服务端不重复

1、消费者-至少一次

在这里插入图片描述

2、消费者-至多一次

在这里插入图片描述

三、生产者API

send()异步发送

异步发送,可以理解是快递员把快递放入菜鸟驿站或者蜂巢,就可以去做别的事情了,菜鸟驿站或蜂巢会启动线程池慢慢发送到broker;缓存区可以理解菜鸟驿站
在这里插入图片描述

同步发送

快递员需要把快递发送到broker才会,发送下一个快递
在这里插入图片描述

四、实战

doc参考:https://kafka.apache.org/documentation/#producerapi

引入jar

在这里插入图片描述

<dependency>
	<groupId>org.apache.kafka</groupId>
	<artifactId>kafka-clients</artifactId>
	<version>3.2.1</version>
</dependency>

docker run --name nginx -p 7102:7102 7443:7443 7901:7901 9093:9093 9094:9094 9095:9095 -d nginx

五、SpringBoot实战

1、引入包

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2、常用配置

###########【Kafka集群】###########
spring.kafka.bootstrap-servers=112.126.74.249:9092,112.126.74.249:9093

3、基础配置(也是影响rebalance的条件)

#  consumer给broker发送心跳的间隔时间,默认时间3秒,broker接收到心跳如果此时有rebalance发生会通过心跳响应将rebalance方案下发给consumer,这个时间可以稍微短一点
spring.kafka.properties.heartbeat.interval.ms: 3000
# 默认45秒,服务端broker多久感知不到一个consumer心跳就认为他故障了,会将其踢出消费组, 对应的Partition也会被重新分配给其他consumer
# 如果有消费者在session规定时间内没有发送心跳包,kafka就会认为该消费者不可用,开始rebalancing。
spring.kafka.properties.session.timeout.ms:45000
# 默认300秒  ,如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱, 会将其踢出消费组,将分区分配给别的consumer消费
spring.kafka.properties.max.poll.interval.ms:300000

生产者配置

###########【初始化生产者配置】###########
# 重试次数
spring.kafka.producer.retries=0
# 应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1)
spring.kafka.producer.acks=1
# 批量大小
spring.kafka.producer.batch-size=16384
# 提交延时
spring.kafka.producer.properties.linger.ms=0
# 当生产端积累的消息达到batch-size或接收到消息linger.ms后,生产者就会将消息提交给kafka
# linger.ms为0表示每接收到一条消息就提交给kafka,这时候batch-size其实就没用了
# 生产端缓冲区大小
spring.kafka.producer.buffer-memory = 33554432
# Kafka提供的序列化和反序列化类
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 自定义分区器
# spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner

消费者配置

###########【初始化消费者配置】###########
# 默认的消费组ID
spring.kafka.consumer.properties.group.id=defaultConsumerGroup
# 是否自动提交offset
spring.kafka.consumer.enable-auto-commit=true
# 提交offset延时(接收到消息后多久提交offset)
spring.kafka.consumer.auto.commit.interval.ms=1000
# 当kafka中没有初始offset或offset超出范围时将自动重置offset
# earliest:重置为分区中最小的offset;
# latest:重置为分区中最新的offset(消费分区中新产生的数据);
# none:只要有一个分区不存在已提交的offset,就抛出异常;
spring.kafka.consumer.auto-offset-reset=latest
# 消费会话超时时间(超过这个时间consumer没有发送心跳,就会触发rebalance操作)
spring.kafka.consumer.properties.session.timeout.ms=120000
# 消费请求超时时间
spring.kafka.consumer.properties.request.timeout.ms=180000
# Kafka提供的序列化和反序列化类
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
# 消费端监听的topic不存在时,项目启动会报错(关掉)
spring.kafka.listener.missing-topics-fatal=false
# 设置批量消费
# spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
# 默认值500  一次poll最大拉取消息的条数,如果消费者处理速度很快,可以设置大点,如果处理速度一般,可以设置小点
# spring.kafka.consumer.max-poll-records=50

4、简单配置

spring:
  kafka:
    #    bootstrap-servers: 192.168.0.44:9093,192.168.0.44:9094,192.168.0.44:9095
    #bootstrap-servers: 192.168.0.44:19092
    bootstrap-servers: 10.80.1.215:9092
    listener:
      # 未发现topic时不报错: 自动创建topic需要设置未false
      missing-topics-fatal: false
    producer:
      # 设置大于0的值,则客户端会将发送失败的记录重新发送
      retries: 0 
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      acks: 1
      #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。16M
      batch-size: 16384
      # 设置生产者内存缓冲区的大小。#32M
      buffer-memory: 33554432
      # 指定消息key和消息体的编解码方式 值的序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      # 消费者组
      group-id: iot-group
      # 自动提交的时间间隔  刷新间隔时间,负值失败时候刷新,0每次发送后刷新
      auto-commit-interval: 100
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移
      enable-auto-commit: true
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录  当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
      auto-offset-reset: earliest
      # 在侦听器容器中运行的线程数。
      concurrency: 6
      # 指定消息key和消息体的编解码方式 值的序列化方式
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
  properties:
      max:
        poll:
          interval:
            ms1: 600000

5、简单生产者

@RestController
public class KafkaProducer {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;// 发送消息
    @GetMapping("/kafka/normal/{message}")
    public void sendMessage1(@PathVariable("message") String normalMessage) {
        kafkaTemplate.send("topic1", normalMessage);
    }
}

通过key指定分片规则

kafkaTemplate.send(topic, key, jsonValue);

6、简单消费者

@Component
public class KafkaConsumer {
    // 消费监听
    @KafkaListener(topics = {"topic1"})
    public void onMessage1(ConsumerRecord<?, ?> record){
        // 消费的哪个topic、partition的消息,打印出消息内容
        System.out.println("简单消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
    }
}

/* 指定多个主题, 并且指定线程数 */
/**
     * 发送消息
     * @param consumerRecord 消息记录
     * @param topicGroupId 消费组
     */
    @KafkaListener(topics = "#{'${mq.alarm.inner.topic.name}'.split(',')}",errorHandler = "consumerIotAlarmAwareErrorHandler",concurrency = "3")
    public void consumer(ConsumerRecord<?, String> consumerRecord, @Header(KafkaHeaders.GROUP_ID) String topicGroupId) {
        execute(consumerRecord, false);
    }

concurrency = "3"表示启动三个线程,即三个消费者,也就是该主题会有三个消费者同时消费,如果多个Partition,则分别对应一个消费者
在这里插入图片描述

7、调用发送

可以看到监听器消费成功,
在这里插入图片描述

六、高级实战-生产者

1、生产者回调

方法一:

kafkaTemplate提供了一个回调方法addCallback,我们可以在回调方法中监控消息是否发送成功 或 失败时做补偿处理,有两种写法,

@GetMapping("/kafka/callbackOne/{message}")
public void sendMessage2(@PathVariable("message") String callbackMessage) {
    kafkaTemplate.send("topic1", callbackMessage).addCallback(success -> {
        // 消息发送到的topic
        String topic = success.getRecordMetadata().topic();
        // 消息发送到的分区
        int partition = success.getRecordMetadata().partition();
        // 消息在分区内的offset
        long offset = success.getRecordMetadata().offset();
        System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
    }, failure -> {
        System.out.println("发送消息失败:" + failure.getMessage());
    });
}

方法二:

@GetMapping("/kafka/callbackTwo/{message}")
public void sendMessage3(@PathVariable("message") String callbackMessage) {
    kafkaTemplate.send("topic1", callbackMessage).addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
        @Override
        public void onFailure(Throwable ex) {
            System.out.println("发送消息失败:"+ex.getMessage());
        }
 
        @Override
        public void onSuccess(SendResult<String, Object> result) {
            System.out.println("发送消息成功:" + result.getRecordMetadata().topic() + "-"
                    + result.getRecordMetadata().partition() + "-" + result.getRecordMetadata().offset());
        }
    });
}

2、自定义分区

我们知道,kafka中每个topic被划分为多个分区,那么生产者将消息发送到topic时,具体追加到哪个分区呢?这就是所谓的分区策略,Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。其路由机制为:

① 若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区;

② 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;

③ patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition;

※ 我们来自定义一个分区策略,将消息发送到我们指定的partition,首先新建一个分区器类实现Partitioner接口,重写方法,其中partition方法的返回值就表示将消息发送到几号分区,

public class CustomizePartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 自定义分区规则(这里假设全部发到0号分区)
        // ......
        return 0;
    }@Override
    public void close() {}@Override
    public void configure(Map<String, ?> configs) {}
}

在application.propertise中配置自定义分区器,配置的值就是分区器类的全路径名,

# 自定义分区器
spring.kafka.producer.properties.partitioner.class=com.felix.kafka.producer.CustomizePartitioner

3、事务提交

如果在发送消息时需要创建事务,可以使用 KafkaTemplate 的 executeInTransaction 方法来声明事务,

@GetMapping("/kafka/transaction")
public void sendMessage7(){
    // 声明事务:后面报错消息不会发出去
    kafkaTemplate.executeInTransaction(operations -> {
        operations.send("topic1","test executeInTransaction");
        throw new RuntimeException("fail");
    });// 不声明事务:后面报错但前面消息已经发送成功了
   kafkaTemplate.send("topic1","test executeInTransaction");
   throw new RuntimeException("fail");
}

七、高级实战-消费者

1、指定topic、partition、offset消费

前面我们在监听消费topic1的时候,监听的是topic1上所有的消息,如果我们想指定topic、指定partition、指定offset来消费呢?也很简单,@KafkaListener注解已全部为我们提供,


/**
* @Title 指定topic、partition、offset消费
* @Description 同时监听topic1和topic2,监听topic1的0号分区、topic2的 "0号和1号" 分区,指向1号分区的offset初始值为8
* @Param [record]
* @return void
**/
@KafkaListener(id = "consumer1",groupId = "felix-group",topicPartitions = {
       @TopicPartition(topic = "topic1", partitions = { "0" }),
       @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "8"))
})
public void onMessage2(ConsumerRecord<?, ?> record) {
   System.out.println("topic:"+record.topic()+"|partition:"+record.partition()+"|offset:"+record.offset()+"|value:"+record.value());
}

属性解释:

① id:消费者ID;

② groupId:消费组ID;

③ topics:监听的topic,可监听多个;

④ topicPartitions:可配置更加详细的监听信息,可指定topic、parition、offset监听。

上面onMessage2监听的含义:监听topic1的0号分区,同时监听topic2的0号分区和topic2的1号分区里面offset从8开始的消息。

注意:topics和topicPartitions不能同时使用;

2、批量消费

设置application.prpertise开启批量消费即可,

# 设置批量消费
spring.kafka.listener.type=batch
# 批量消费每次最多消费多少条消息
spring.kafka.consumer.max-poll-records=50

接收消息时用List来接收,监听代码如下,

@KafkaListener(id = "consumer2",groupId = "felix-group", topics = "topic1")
public void onMessage3(List<ConsumerRecord<?, ?>> records) {
   System.out.println(">>>批量消费一次,records.size()="+records.size());
   for (ConsumerRecord<?, ?> record : records) {
       System.out.println(record.value());
   }
}

3、ConsumerAwareListenerErrorHandler 异常处理器

通过异常处理器,我们可以处理consumer在消费时发生的异常。

新建一个 ConsumerAwareListenerErrorHandler 类型的异常处理方法,用@Bean注入,BeanName默认就是方法名,然后我们将这个异常处理器的BeanName放到@KafkaListener注解的errorHandler属性里面,当监听抛出异常的时候,则会自动调用异常处理器,

// 新建一个异常处理器,用@Bean注入
@Bean
public ConsumerAwareListenerErrorHandler consumerAwareErrorHandler() {
   return (message, exception, consumer) -> {
       System.out.println("消费异常:"+message.getPayload());
       return null;
   };
}// 将这个异常处理器的BeanName放到@KafkaListener注解的errorHandler属性里面
@KafkaListener(topics = {"topic1"},errorHandler = "consumerAwareErrorHandler")
public void onMessage4(ConsumerRecord<?, ?> record) throws Exception {
   throw new Exception("简单消费-模拟异常");
}// 批量消费也一样,异常处理器的message.getPayload()也可以拿到各条消息的信息
@KafkaListener(topics = "topic1",errorHandler="consumerAwareErrorHandler")
public void onMessage5(List<ConsumerRecord<?, ?>> records) throws Exception {
   System.out.println("批量消费一次...");
   throw new Exception("批量消费-模拟异常");
}

执行看一下效果,

4、消息过滤器

消息过滤器可以在消息抵达consumer之前被拦截,在实际应用中,我们可以根据自己的业务逻辑,筛选出需要的信息再交由KafkaListener处理,不需要的消息则过滤掉。

配置消息过滤只需要为 监听器工厂 配置一个RecordFilterStrategy(消息过滤策略),返回true的时候消息将会被抛弃,返回false时,消息能正常抵达监听容器。

@Component
public class KafkaConsumer {
   @Autowired
   ConsumerFactory consumerFactory;// 消息过滤器
   @Bean
   public ConcurrentKafkaListenerContainerFactory filterContainerFactory() {
       ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
       factory.setConsumerFactory(consumerFactory);
       // 被过滤的消息将被丢弃
       factory.setAckDiscarded(true);
       // 消息过滤策略
       factory.setRecordFilterStrategy(consumerRecord -> {
           if (Integer.parseInt(consumerRecord.value().toString()) % 2 == 0) {
               return false;
           }
           //返回true消息则被过滤
           return true;
       });
       return factory;
   }// 消息过滤监听
   @KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory")
   public void onMessage6(ConsumerRecord<?, ?> record) {
       System.out.println(record.value());
   }
}

上面实现了一个"过滤奇数、接收偶数"的过滤策略,我们向topic1发送0-99总共100条消息,看一下监听器的消费情况,可以看到监听器只消费了偶数,
在这里插入图片描述

5、消息转发

在实际开发中,我们可能有这样的需求,应用A从TopicA获取到消息,经过处理后转发到TopicB,再由应用B监听处理消息,即一个应用处理完成后将该消息转发至其他应用,完成消息的转发。

在SpringBoot集成Kafka实现消息的转发也很简单,只需要通过一个@SendTo注解,被注解方法的return值即转发的消息内容,如下,

/**
* @Title 消息转发
* @Description 从topic1接收到的消息经过处理后转发到topic2
* @Param [record]
* @return void
**/
@KafkaListener(topics = {"topic1"})
@SendTo("topic2")
public String onMessage7(ConsumerRecord<?, ?> record) {
   return record.value()+"-forward message";
}

6、定时启动、停止监听器

默认情况下,当消费者项目启动的时候,监听器就开始工作,监听消费发送到指定topic的消息,那如果我们不想让监听器立即工作,想让它在我们指定的时间点开始工作,或者在我们指定的时间点停止工作,该怎么处理呢——使用KafkaListenerEndpointRegistry,下面我们就来实现:

① 禁止监听器自启动;

② 创建两个定时任务,一个用来在指定时间点启动定时器,另一个在指定时间点停止定时器;

新建一个定时任务类,用注解@EnableScheduling声明,KafkaListenerEndpointRegistry 在SpringIO中已经被注册为Bean,直接注入,设置禁止KafkaListener自启动,

@EnableScheduling
@Component
public class CronTimer {/**
    * @KafkaListener注解所标注的方法并不会在IOC容器中被注册为Bean,
    * 而是会被注册在KafkaListenerEndpointRegistry中,
    * 而KafkaListenerEndpointRegistry在SpringIOC中已经被注册为Bean
    **/
   @Autowired
   private KafkaListenerEndpointRegistry registry;@Autowired
   private ConsumerFactory consumerFactory;// 监听器容器工厂(设置禁止KafkaListener自启动)
   @Bean
   public ConcurrentKafkaListenerContainerFactory delayContainerFactory() {
       ConcurrentKafkaListenerContainerFactory container = new ConcurrentKafkaListenerContainerFactory();
       container.setConsumerFactory(consumerFactory);
       //禁止KafkaListener自启动
       container.setAutoStartup(false);
       return container;
   }// 监听器
   @KafkaListener(id="timingConsumer",topics = "topic1",containerFactory = "delayContainerFactory")
   public void onMessage1(ConsumerRecord<?, ?> record){
       System.out.println("消费成功:"+record.topic()+"-"+record.partition()+"-"+record.value());
   }// 定时启动监听器
   @Scheduled(cron = "0 42 11 * * ? ")
   public void startListener() {
       System.out.println("启动监听器...");
       // "timingConsumer"是@KafkaListener注解后面设置的监听器ID,标识这个监听器
       if (!registry.getListenerContainer("timingConsumer").isRunning()) {
           registry.getListenerContainer("timingConsumer").start();
       }
       //registry.getListenerContainer("timingConsumer").resume();
   }// 定时停止监听器
   @Scheduled(cron = "0 45 11 * * ? ")
   public void shutDownListener() {
       System.out.println("关闭监听器...");
       registry.getListenerContainer("timingConsumer").pause();
   }
}

启动项目,触发生产者向topic1发送消息,可以看到consumer没有消费,因为这时监听器还没有开始工作,

在这里插入图片描述

11:42分监听器启动开始工作,消费消息,

在这里插入图片描述

11:45分监听器停止工作,

在这里插入图片描述

7、消费多个主题

参考:@KafkaListener动态指定多个topic

// 配置在yml文件或者properties文件中
topics: "admin,login,client"

// 代码
@KafkaListener(topics = "#{'${topics}'.split(',')}")
@KafkaListener(topics = "#{'${topics}'.split(',')}",concurrency = "#{'${topics}'.split(',').length}")
@KafkaListener(topics = {DeviceMqConfig.TOPIC_HELMET_ALARM_INNER, DeviceMqConfig.TOPIC_HELMET_DATA_INNER}, concurrency = "3")

8、消息确认

参考:Spring Kafka消费模式(single, batch)及确认模式(自动、手动)示例

9、使用变量

@KafkaListener(topics = {DeviceMqConfig.TOPIC_PROPERTY}, concurrency = "#{mqConfig.getPropertyConcurrency()}")
    public void consumer(ConsumerRecord<?, String> consumerRecord) {
        this.checkMessage(consumerRecord);
    }

10、通过key计算partition分区

每个topic可以设置多个partition,每个partition中数据是顺序的,然后可以send的时候通过制定key,根据kafka内部的hash算法路由到对应的partition
场景:比如iot设备数据上报,可以根据设备code作为key,这样相同设备的数据在同一个partition;

public void send(String topic, String jsonValue, String tagId, String key) {
        List<Header> headers = null;
        if (StrUtil.isNotBlank(tagId)) {
            headers = Collections.singletonList(new RecordHeader(IotConstant.MQ_TAG, IotConstant.MQ_TAG_VALUE.getBytes(StandardCharsets.UTF_8)));
        }
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, null, null, key, jsonValue, headers);
        kafkaTemplate.send(producerRecord);
    }

11、concurrency

可以再yml中统一配置,也可以在@KafkaListener中单独制定

@KafkaListener(topics = {DeviceMqConfig.TOPIC_PROPERTY_INNER}, concurrency = "#{mqConfig.getPropertyConcurrency()}")
@KafkaListener(topics = {DeviceMqConfig.TOPIC_PROPERTY_INNER}, concurrency = "2")

项目中总的消费者线程数量为:节点 * concurrency * @KafkaListener的数量(默认监听全部的partition)

因为一个partition只能被一个消费者消费,所以如下

  1. 当concurrency < partition 的数量,会出现消费不均的情况,一个消费者的线程可能消费多个partition 的数据

  2. 当concurrency = partition 的数量,最佳状态,一个消费者的线程消费一个 partition 的数据

  3. 当concurrency > partition 的数量,会出现有的消费者的线程没有可消费的partition, 造成资源的浪费

由此上说明,定义partition为4,并且有两个节点,则建议配置concurrency = 2;4 = 2*2
如果只有一个节点,则配置concurrency = 4,则每个消费者消费一个partition

比如设置concurrency =1,partition=4则如下
在这里插入图片描述

八、高级实战-消息确认

1、参考

【kafka异常】使用Spring-kafka遇到的坑

2、概述

手动提交模式

  • MANUAL poll()拉取一批消息,处理完业务后,手动调用Acknowledgment.acknowledge()先将offset存放到map本地缓存,在下一次poll之前从缓存拿出来批量提交
  • MANUAL_IMMEDIATE 每处理完业务手动调用Acknowledgment.acknowledge()后立即提交
  • RECORD 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
  • BATCH 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
  • TIME 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
  • COUNT 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
    COUNT_TIME TIME或COUNT满足其中一个时提交

MANUAL:poll()拉取一批消息,处理完业务后,手动调用Acknowledgment.acknowledge()先将offset存放到map本地缓存,在下一次poll之前从缓存拿出来批量提交。

3、新建手动提交container配置

为了兼容默认的自动提交模式,所以需要单独定义consumerFactory和KafkaListenerContainerFactory,切记如下类必须用@Configuration注解,不能用@Component,否则会提示存在两个ConsumerFactory的bean

@Slf4j
@Configuration
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnProperty(name = "mq.pipeline", havingValue = IotConstant.MQ_KAFKA)
public class KafkaReceiverConfig {

	@Resource
    private KafkaProperties properties;
    /*@Resource()
    private ConsumerFactory<String,String> consumerFactory;*/
    @Resource
    private KafkaTemplate<String,Object>  kafkaTemplate;

    @Primary
    public ConsumerFactory<Object, Object> consumerFactory() {
        Map<String, Object> map =  properties.buildConsumerProperties();
        return new DefaultKafkaConsumerFactory<>( map);
    }
    /**
     * 针对tag消息过滤
     * producer 将tag写进header里
     * @return ConcurrentKafkaListenerContainerFactory
     */
    @Bean
    public ConcurrentKafkaListenerContainerFactory<String,String> filterContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String,String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        // 被过滤的消息将被丢弃
        factory.setAckDiscarded(true);

        factory.setRecordFilterStrategy(consumerRecord -> {
            if (Optional.ofNullable(consumerRecord.value()).isPresent()) {
                for (Header header : consumerRecord.headers()) {
                    if (header.key().equals(IotConstant.MQ_TAG) && new String(header.value()).equals(new String(IotConstant.MQ_TAG_VALUE.getBytes(StandardCharsets.UTF_8)))) {
                        return false;
                    }
                }
            }
            //返回true将会被丢弃
            return true;
        });
        return factory;
    }

    /**
     * 创建一个新的消费者工厂
     * 但是修改为不自动提交
     *
     * @return ConsumerFactory<Object, Object>
     */
    @Bean
    public ConsumerFactory<Object, Object> kafkaManualConsumerFactory() {
        Map<String, Object> map =  properties.buildConsumerProperties();
        map.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        return new DefaultKafkaConsumerFactory<>( map);
    }

    /**
     * 手动提交的监听器工厂 (使用的消费组工厂必须 kafka.consumer.enable-auto-commit = false)
     * @return
     */
    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaManualAckListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(kafkaManualConsumerFactory());
        /* 设置提交偏移量的方式 当Acknowledgment.acknowledge()侦听器调用该方法时,立即提交偏移量
        MANUAL	poll()拉取一批消息,处理完业务后,手动调用Acknowledgment.acknowledge()先将offset存放到map本地缓存,在下一次poll之前从缓存拿出来批量提交
        MANUAL_IMMEDIATE	每处理完业务手动调用Acknowledgment.acknowledge()后立即提交 */
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        return factory;
    }


    @Bean
    public ConsumerAwareListenerErrorHandler consumerIotAlarmAwareErrorHandler() {
        return new ConsumerAwareListenerErrorHandler() {

            @Override
            public Object handleError(Message<?> message, ListenerExecutionFailedException e, Consumer<?, ?> consumer) {
                log.error("consumerAwareErrorHandler receive : {}, error:{}",message.getPayload(),e);

                //获取消息处理异常主题
                MessageHeaders headers = message.getHeaders();
                Map<TopicPartition, Long> offsetsToReset = new HashMap<>();
                String topic="iotAlarmInner"+ KafkaAlarmListener.TOPIC_DLT;
                //放入死信队列
                kafkaTemplate.send(topic,message.getPayload());
                return message;
            }
        };
    }
}

名词解释:

4、消费者配置制定container

/**
     * 消费者
     *
     * @param consumerRecord 消息记录
     */
    @KafkaListener(topics = {DeviceMqConfig.TOPIC_DOWNLOAD_INNER}, concurrency = "#{mqConfig.getEventConcurrency()}" ,containerFactory = "kafkaManualAckListenerContainerFactory")
    public void consumer(ConsumerRecord<?, String> consumerRecord, Acknowledgment ack) {
    	// 手动提交
        ack.acknowledge();
    }

5、踩坑报错

No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.

问题原因:

不能再配置中既配置kafka.consumer.enable-auto-commit=true 自动提交; 然后又在监听器中使用手动提交

解决方案:

  • 将自动提交关掉,或者去掉手动提交;

  • 如果你想他们都同时存在,某些情况自动提交;某些情况手动提交; 那你创建 一个新的

  • consumerFactory 将它的是否自动提交设置为false;

消费者在消费消息的过程中,配置参数设置为不自动提交offset,也就是手动提交,在消费完数据之后如果不手动提交offset,那么在程序中和kafak中的数据会如何被处理呢?

参考:对于kafka消费后不提交offset的分析总结

  • 1.如果在消费kafka的数据过程中,一直没有提交offset,那么在此程序运行的过程中它不会重复消费。但是如果重启之后,就会重复消费之前没有提交offset的数据。

  • 2.如果在消费的过程中有几条或者一批数据数据没有提交offset,后面其他的消息消费后正常提交offset,那么服务端会更新为消费后最新的offset,不会重新消费,就算重启程序也不会重新消费。

  • 3.消费者如果没有提交offset,程序不会阻塞或者重复消费,除非在消费到这个你不想提交offset的消息时你尝试重新初始化一个客户端消费者,即可再次消费这个未提交offset的数据。因为客户端也记录了当前消费者的offset信息,所以程序会在每次消费了数据之后,自己记录offset,而手动提交到服务端的offset与这个并没有关系,所以程序会继续往下消费。在你重新初始化客户端消费者之后,会从服务端得到最新的offset信息记录到本地。所以说如果当前的消费的消息没有提交offset,此时在你重新初始化消费者之后,可得到这条未提交消息的offset,从此位置开始消费。
    总结就是如果不提交或者抛异常,相当于一直没有提交offset,在次程序运行过程中不会重复消费,除非是重启,或者有新的消费者加入导致数据均衡的时候才会再次触发消费;而且有新的消息正常消费提交offset之后,服务端就会更新罪行的offset,这样就算程序重启或者均衡也不会再消费了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值