消息队列是怎么保证消息可靠传递不丢失的?

200 阅读3分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

rabbitMQ消息丢失

AMQP事务

如何保证生产者发送的消息持久化到了硬盘中? 将确保代理信息的行为包装到事务中.当把信道设置成事务模式后,生产者通过信道发送的消息后,还有其他的AMQP命令,如分发到队列中,后面的所有操作都取决于第一条操作是否成功,如果成功,才会执行余下的所有AMQP命令.如果失败,其他的AMQP命令将不会执行.

但是,事务几乎吸干了Rabbit的性能,并且会生产者的程序进行同步,无法进行异步操作.如果想要更好的性能和异步通信,那么需要confirm模式.

confirm可靠性

如果想要保证发送消息时不丢失,那么就可以开启confirm机制,当传送到broker时,broker会返回应答给生产者。如果没有返回,就代表消息丢失。

confirm是异步的,所以效率比事务高很多。

如何开启

channel.confirmSelect() 开启确认模式

channel.addConfirmListener()方法监听成功或者失败的返回结果,根据不同的结果对消息进行重发,记录等操作

代码

// 开启确认模式
        channel.confirmSelect();
        //添加监听方法
        channel.addConfirmListener(new ConfirmListener() {
            // 返回应答结果
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                System.out.println("成功发送");
                channel.basicPublish("",QUEUE_NAME,null,"message".getBytes());
            }

            // 未返回应答结果
            @Override
            public void handleNack(long l, boolean b) throws IOException {
                System.out.println("发送失败,请重新发送");
            }
        });

channel.basicPublish("",QUEUE_NAME,null,"start".getBytes());

在上面的例子中,如果发送成功,就会重复发送,用来验证是否触发成功结果

RabbitMQ中的消息如图所示,短短两三秒,就发送了近3w条消息,说明成功触发了。

持久化

想要保证在RabbitMQ中消息不丢失,需要开启持久化

重启RabbitMQ服务器,里面的队列,交换机连同消息一起消失。将队列,交换机的durable属性设置为true,就会在重启后重新创建该队列和交换机(持久化交换机和持久化队列)。

想要持久化消息:

  • 将投递模式选项设置为2,来将消息标记为持久化(消息的状态是持久化的)

  • 消息非发布到持久化的交换机和到达持久化的队列中(进入持久化容器)

  • RabbitMQ持久化机制

    持久化消息发布到持久化交换机,将消息写到日志文件后,然后返回响应。如果这条消息发送到了非持久话的队列,那么就会从日志文件中删除。当消费了持久化队列中的持久化消息,日志文件中就会将消息标记为待垃圾回收。

    持久化消息通信会付出性能降低的代价

取消自动ack

在生产者发送消息到RabbitMQ时我们可以通过ack来确认消息是否到达了服务端,与之类似的是,消费者在消费消息时同样提供手动ack模式。默认情况下,消费者从队列中获取消息后会自动ack,我们可以通过手动ack来保证消费者主动的控制ack行为,这样我们可以避免业务异常导致消息丢失的情况。

**

DeliverCallback deliverCallback = new DeliverCallback() {
    @Override
    public void handle(String consumerTag, Delivery message) throws IOException {
        try {
            byte[] body = message.getBody();
            String messageContent = new String(body, StandardCharsets.UTF_8);
            if("error".equals(messageContent)){
                throw new RuntimeException("业务异常");
            }
            log.info("收到的消息内容:{}",messageContent);
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        }catch (Exception e){
            log.info("消费消息失败!重回队列!");
            channel.basicNack(message.getEnvelope().getDeliveryTag(),false,true);
        }
    }
};
CancelCallback cancelCallback = new CancelCallback() {
    @Override
    public void handle(String consumerTag) throws IOException {
        log.info("取消订阅:{}",consumerTag);
    }
};
channel.basicConsume("confirm.queue",false,deliverCallback,cancelCallback);

上面的代码我们通过手动ack来控制消息是否被成功消费,当收到的消息内容为error时,我们手动抛出异常。在异常处理中将requeue设置成true,这将使该消息重新回到队列。运行代码可以看到,该消息一直在重复的打印出来。