Kafka 消息确认机制 保证不丢失及手动确认消息
- Producer保证消息传输过程中不丢失
- 事务
- Consumer手动确认消息
- 全局配置
- auto.offset.reset值含义解释
- spring.kafka.listener.ack-mode
- 自建``ConcurrentKafkaListenerContainerFactory``
- group 需要注意的地方
- 总结
Producer保证消息传输过程中不丢失
org.springframework.boot.autoconfigure.kafka.KafkaProperties
中可以看见所有配置项,这次讲讲如何保证消息在传输过程中
不因为服务器突然宕机而没有收到消息
,但producer
却认为消息已经发送成功
的问题。spring.kafka.producer.acks
这个配置项有3个
可选,分别是0
,1
,-1
。
ack=0
:就是说消息只要通过网络发送出去就不会再管,无论是否被服务器接收到。也就是我只管发,你接到还是没接到我并不关心。ack=1(默认)
:producer(消息发送方)
收到leader副本
的确认
,才会认为发送成功。就像是TCP连接一样,我要收到服务器的ack
才行。ack=-1(ALL)
:leader副本
在返回确认或错误响应之前,会等待所有follower
副本都收到消息,才会认为发送成功。这个ALL
很好理解,就是所有副本都收到消息,才会认为发送成功。
事务
关于多条消息发送需要原子性的事务操作,需要配置
spring.kafka.producer.transaction-id-prefix
来开启事务(否则即使抛出异常消息还是会被发出去
),但需要注意的是,如果开启事务
,那么之后就必须使用事务方式kafkaTemplate.executeInTransaction
或@Transactional
,否则会报错。
kafkaTemplate.executeInTransaction(operations ->{
operations.send(topic1,message);
operations.send(topic2,message);
operations.send(topic3,message);
throw new RuntimeException("error");//模拟报错
});
Consumer手动确认消息
仅仅保证消息传输过程中不丢失还不够,也许消息在消费过程中(拿到消息还没有做完事),
consumer
的服务器挂了或者异常了呢?这条消息同样会丢失。org.springframework.boot.autoconfigure.kafka.KafkaProperties
中同样有配置可以修改全局设置,如果不想变更全局设置,可以自己写个Bean。
全局配置
spring.kafka.consumer.enable-auto-commit
=falsespring.kafka.consumer.auto-offset-reset
=earliestspring.kafka.listener.ack-mode
=manual_immediate
auto.offset.reset值含义解释
earliest
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费latest
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据none
topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
spring.kafka.listener.ack-mode
org.springframework.kafka.listener.ContainerProperties.AckMode
中可以看见所有的值。
想要手动确认消息,要么改为MANUAL
,要么改为MANUAL_IMMEDIATE
.
关于MANUAL_IMMEDIATE
和MANUAL
的区别就是MANUAL
不会立即提交,它是获取poll中的值后设置record,最后批量提交。
@Component
public class KafkaConsumer {
private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);
@KafkaListener(topics = "first-topic",groupId="test-1",autoStartup="true")
public void read(ConsumerRecord<?,?> record, Acknowledgment ack){
logger.info(record.topic());
logger.info((String) record.key());
logger.info((String) record.value());
logger.info(String.valueOf(record.headers()));
//手动确认消息
//拒绝消息,过1000毫秒后重新消费,也就是延迟处理
//ack.nack(1000);
}
}
自建ConcurrentKafkaListenerContainerFactory
在不想修改
全局配置
的情况下,可以自己创建Factory
,通过指定containerFactory
使用
@EnableKafka
@Configuration
public class KafkaConfig {
@Bean("manualListenerContainerFactory")
ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
ConsumerFactory<?, ?> kafkaConsumerFactory) {
ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
configurer.configure(factory, (ConsumerFactory<Object, Object>) kafkaConsumerFactory);
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
factory.getContainerProperties().setAckTime(10000);
return factory;
}
}
@Component
public class KafkaConsumer {
private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);
@KafkaListener(containerFactory = "manualListenerContainerFactory" ,topics = "first-topic",groupId="test-1",autoStartup="true")
public void read(ConsumerRecord<?,?> record, Acknowledgment ack){
logger.info(record.topic());
logger.info((String) record.key());
logger.info((String) record.value());
logger.info(String.valueOf(record.headers()));
ack.acknowledge();
//拒绝消息,过1000毫秒后重新消费,也就是延迟处理
//ack.nack(1000);
}
}
group 需要注意的地方
groupId="test-${random.int}"
,为了分布式情况下,多个服务不轮询处理,我们常常会随机groupId
,但这种方式会让groupId
每次都随机,那么产生的结果是,虽然达到了广播
的效果,但每次重启都默认所有消息都没有确认过
,因为groupId
每次重启都是随机的。
所以groupId
最好是按照服务名
或hostname
等来写。
总结
- 如果消息没有经过确认或打回,那么这个消息将在下次启动
consumer
的时候根据auto.offset.reset
重新消费消息。- 如果
groupId
不同,那么消息确认是不会影响其他group的
,哪怕是在同个topic
中。(也就是说,groupId
为test-1
的consumer
没有确认消息,而test-2
的consumer
确认了消息,那么仅test-1
的consumer
下次启动会重新读取到消息。)Acknowledgment#nack
可以将消息直接打回,延迟处理。- 如果没有
Acknowledgment#ack
也没有Acknowledgment#nack
,那么消息只有在下次重启consumer
的时候才会被重新消费。