概述
消息队列丢失消息是个经典的问题,也是我们在开发当中容易忽略的问题,最重要是面试当中必考题。消息队列总体上分为三部分:生产者(Producer),消费者(Consumer),消息队列服务端(broker)。不幸的是在使用不当情况下,三个环节都有可能丢失消息,今天我们就来讨论下为什么会丢消息,以及丢消息的解决方案。
producer丢失消息问题
Producer的作用是向Broker发送消息的。例如在这样一个场景: Producer已经向Broker发送了一条消息,在消息传输到Broker的途中,还没到达,这个时候Broker宕机了。很显然在这种情况下,Producer认为自己已经发送消息给到Broker,同时Broker并不知道Producer发送了消息,这个时候Producer其实丢消息了
解决方案: 其实问题也很简单,无非就是Producer和Broker之间缺乏一种确认机制。如果在Broker收到消息通知一下Producer这个问题就解决了,如果一条消息长时间没有应答,那么就说明Producer发送的消息没有到达Broker,这样Producer再补发一条就是了(网络延迟情况下导致发送重复消息)
//发送结果回调处理
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宕机了,这个消息岂不是丢失了。
解决方案:其实我们看图知道,这个情况是因为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();
});
}
}