消息队列-kafka消息丢失问题

3,451 阅读3分钟

概述

消息队列丢失消息是个经典的问题,也是我们在开发当中容易忽略的问题,最重要是面试当中必考题。消息队列总体上分为三部分:生产者(Producer),消费者(Consumer),消息队列服务端(broker)。不幸的是在使用不当情况下,三个环节都有可能丢失消息,今天我们就来讨论下为什么会丢消息,以及丢消息的解决方案。

producer丢失消息问题

Producer的作用是向Broker发送消息的。例如在这样一个场景: Producer已经向Broker发送了一条消息,在消息传输到Broker的途中,还没到达,这个时候Broker宕机了。很显然在这种情况下,Producer认为自己已经发送消息给到Broker,同时Broker并不知道Producer发送了消息,这个时候Producer其实丢消息了

image.png

解决方案: 其实问题也很简单,无非就是Producer和Broker之间缺乏一种确认机制。如果在Broker收到消息通知一下Producer这个问题就解决了,如果一条消息长时间没有应答,那么就说明Producer发送的消息没有到达Broker,这样Producer再补发一条就是了(网络延迟情况下导致发送重复消息)

image.png

//发送结果回调处理
public class ProducerListenerTest implements ProducerListener {
    @Override
    public void onSuccess(ProducerRecord producerRecord, RecordMetadata recordMetadata) {
        System.out.println("发送消息成功:"+recordMetadata);
    }

    @Override
    public void onError(ProducerRecord producerRecord, RecordMetadata recordMetadata, Exception exception) {
        System.out.println("发送消息失败:"+producerRecord+exception);
        //失败记录消息入库,等待批处理
    }
}

//发送消息
@Component
public class ProducerTest {

    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void product(String msg){
        kafkaTemplate.setProducerListener(new ProducerListenerTest());
        kafkaTemplate.send("quickstart-events",msg);
    }

}

//配置
spring.kafka.producer.acks=1  leader分区已经写入,才响应Producer写入成功

Broker丢失消息问题

BroKer的leader分区收到消息以后,马上会响应Producer.但是明明leader分区已经写入了,为什么还会丢消息呢?原因是因为kafka为了提高效率和性能先写到pagecache缓存中,需要等待一批数据统一刷盘进行持久化,试想如果这个时候这个leader所在的Broker宕机了,这个消息岂不是丢失了。

image.png

解决方案:其实我们看图知道,这个情况是因为pagecache还没刷盘导致的单点故障,我们解决掉单点故障问题就可以了,我们只要让一个消息写入大于一个副本就可以了

spring.kafka.producer.acks=2  一个分区两个副本已经写入,才响应Producer写入成功

Consumer丢失消息问题

消息已经从Producer发送成功,Borker也成功持久化消息,自然而然Consumer拉消息肯定能拉取到的,但是Consumer丢失消息丢失消息是怎么回事呢? 其实说的是consumer错误操作offset的一个问题。默认情况下,只要你的消费端程序没有抛异常,就会自动提交当前offset的。举例两种场景:1.为了减少消息堆积,直接将消息处理封装为一个task,然后丢到线程池中,这个时候其实默认提交了offset,如果线程池处理抛了异常,那么线程池里面剩下消息都将丢失。2.错误使用try,异常捕获了,导致提交了offset

解决方案:

@Component
public class ConsumerTest {

    private ExecutorService executorService= Executors.newFixedThreadPool(5);

    //错误代码实例--使用try
    //@KafkaHandler
    //@KafkaListener(topics = "quickstart-events",groupId = "test-consumer-group")
    public void test1(String msg){

        try {
            System.out.println("接收到消息:"+msg);
            int i=1/0;
            //继续处理业务
        }catch (Exception e){
            e.printStackTrace();
        }

    }


    //错误代码实例--异步处理
    //@KafkaHandler
    //@KafkaListener(topics = "quickstart-events",groupId = "test-consumer-group")
    public void test2(String msg){

        executorService.submit(()->{
            System.out.println("接收到消息:"+msg);
            int i=1/0;
        });

    }

    //正常示例--不使用try
    //@KafkaHandler
    //@KafkaListener(topics = "quickstart-events",groupId = "test-consumer-group")
    public void test3(String msg){


        System.out.println("接收到消息:"+msg);
        int i=1/0;
        //继续处理业务
        //...
    }



    //正常示例--手动提交(关闭自动提交)
    @KafkaHandler
    @KafkaListener(topics = "quickstart-events",groupId = "test-consumer-group")
    public void test4(String msg,Acknowledgment ack){

        executorService.submit(()->{
            System.out.println("接收到消息:"+msg);
            //手动提交
            ack.acknowledge();
        });
    }
}