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=false
spring.kafka.consumer.auto-offset-reset=earliest
spring.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_IMMEDIATEMANUAL的区别就是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中。(也就是说,groupIdtest-1consumer没有确认消息,而test-2consumer确认了消息,那么仅test-1consumer下次启动会重新读取到消息。)
  • Acknowledgment#nack可以将消息直接打回,延迟处理。
  • 如果没有Acknowledgment#ack也没有Acknowledgment#nack,那么消息只有在下次重启consumer的时候才会被重新消费。