使用背景 :
与第三方公司的一些基础数据对接,本来是通过定时器拉取数据. 但是项目多了 大了之后 发现偶合太高了. 故想使用中间件 来对这方面进行解耦. 但是不想消费的时候去自动提交, 因为代码中也有逻辑. 如果失败则需要继续消费失败的数据.
版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.5RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
配置
spring:
kafka:
bootstrap-servers: ip:端口
consumer:
group-id: base_consumer_group #群组ID
enable-auto-commit: false #取消自动提交
max-poll-records: 10 #一次最大拉取消息数
auto-offset-reset: earliest # 最早未被消费的offset
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
topics: topics_name_1,topics_name_2 # 监听的topics
编写Bean 为手动提交准备(我是写在启动类中的)
/**
* MANUAL 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
* @param consumerFactory
* @return
*/
@Bean("manualListenerContainerFactory")
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> manualListenerContainerFactory(
ConsumerFactory<String, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.getContainerProperties().setPollTimeout(1500);
factory.setBatchListener(true);
//配置手动提交offset
// 如果 ContainerProperties.AckMode 报错 请查看你的kafka 版本
//如果想要使用其他模式 下面会列出来 请在此处替换即可
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
return factory;
}
各种提交模式
public enum AckMode {
/**
* Commit after each record is processed by the listener.
*/
RECORD,
/**
* Commit whatever has already been processed before the next poll.
*/
BATCH,
/**
* Commit pending updates after
* {@link ContainerProperties#setAckTime(long) ackTime} has elapsed.
*/
TIME,
/**
* Commit pending updates after
* {@link ContainerProperties#setAckCount(int) ackCount} has been
* exceeded.
*/
COUNT,
/**
* Commit pending updates after
* {@link ContainerProperties#setAckCount(int) ackCount} has been
* exceeded or after {@link ContainerProperties#setAckTime(long)
* ackTime} has elapsed.
*/
COUNT_TIME,
/**
* User takes responsibility for acks using an
* {@link AcknowledgingMessageListener}.
*/
MANUAL,
/**
* User takes responsibility for acks using an
* {@link AcknowledgingMessageListener}. The consumer
* immediately processes the commit.
*/
MANUAL_IMMEDIATE,
}
AckMode模式 | 作用 |
---|---|
MANUAL | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交 |
MANUAL_IMMEDIATE | 手动调用Acknowledgment.acknowledge()后立即提交 |
RECORD | 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交 |
BATCH | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交 |
TIME | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交 |
COUNT | 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交 |
COUNT_TIME | TIME或COUNT 有一个条件满足时提交 |
具体代码简易实现
/**
* topics 中 是读取配置 该写法可以监听多个topics
* 如果不设置 manualListenerContainerFactory 下面的ack 会获取不到
* 也可以改成配置的方式 则不需要写Bean 了 两种结果都是一样的
* @param records
* @param ack
*/
@KafkaListener(topics = "#{'${spring.kafka.consumer.topics}'.split(',')}",containerFactory = "manualListenerContainerFactory")
public void listen(List<ConsumerRecord<String,String>> records, Acknowledgment ack) {
try {
//解析消息
log.info("kafka成功,当前成功的批次。data:{}", records);
for (ConsumerRecord record : records) {
//处理消息
log.info("处理消息 : {}", record);
}
//手动提交 至关重要
ack.acknowledge();
} catch (Exception e) {
log.error("kafka失败,当前失败的批次。data:{}", records);
e.printStackTrace();
ack.nack(500);
}
}