参考
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、消费多个主题
// 配置在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只能被一个消费者消费,所以如下
-
当concurrency < partition 的数量,会出现消费不均的情况,一个消费者的线程可能消费多个partition 的数据
-
当concurrency = partition 的数量,最佳状态,一个消费者的线程消费一个 partition 的数据
-
当concurrency > partition 的数量,会出现有的消费者的线程没有可消费的partition, 造成资源的浪费
由此上说明,定义partition为4,并且有两个节点,则建议配置concurrency = 2;4 = 2*2
如果只有一个节点,则配置concurrency = 4,则每个消费者消费一个partition
比如设置concurrency =1,partition=4则如下
八、高级实战-消息确认
1、参考
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中的数据会如何被处理呢?
-
1.如果在消费kafka的数据过程中,一直没有提交offset,那么在此程序运行的过程中它不会重复消费。但是如果重启之后,就会重复消费之前没有提交offset的数据。
-
2.如果在消费的过程中有几条或者一批数据数据没有提交offset,后面其他的消息消费后正常提交offset,那么服务端会更新为消费后最新的offset,不会重新消费,就算重启程序也不会重新消费。
-
3.消费者如果没有提交offset,程序不会阻塞或者重复消费,除非在消费到这个你不想提交offset的消息时你尝试重新初始化一个客户端消费者,即可再次消费这个未提交offset的数据。因为客户端也记录了当前消费者的offset信息,所以程序会在每次消费了数据之后,自己记录offset,而手动提交到服务端的offset与这个并没有关系,所以程序会继续往下消费。在你重新初始化客户端消费者之后,会从服务端得到最新的offset信息记录到本地。所以说如果当前的消费的消息没有提交offset,此时在你重新初始化消费者之后,可得到这条未提交消息的offset,从此位置开始消费。
总结就是如果不提交或者抛异常,相当于一直没有提交offset,在次程序运行过程中不会重复消费,除非是重启,或者有新的消费者加入导致数据均衡的时候才会再次触发消费;而且有新的消息正常消费提交offset之后,服务端就会更新罪行的offset,这样就算程序重启或者均衡也不会再消费了