RabbitMQ 消息可靠性

消息丢失的场景有哪些?

分析导致RabbitMQ消息丢失的场景,不妨先看看一条消息从生产者发送到消费者消费的整个过程。

img

从图中我们可以看到消息从生产到被消费的整个过程:

① 生产者发送消息给交换机。

② 交换机将消息路由到队列。

③ 消费者从队列中消费消息。

以上三步中的每一步都可能导致消息丢失。

1. 消息发送过程导致的消息丢失

生产者将消息发送给交换机时,可能由于网络波动等原因导致消息不能正确到达交换机,导致消息丢失。

解决方案:发送方确认机制,即消息正确到达交换机后,RabbitMQ向生产者发送确认消息

代码示例:

以 Spring Boot 整合 RabbitMQ 为例

注:关于 Spring Boot 整合 RabbitMQ 可以参考我的另一篇博文

添加如下配置:

# 开启发送方确认机制
spring.rabbitmq.publisher-confirms=true

创建一个Controller接口测试消息发送

public class RabbitMQController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostMapping("/sendMessage")
    public void sendMessage(){		
        // 设置确认回调方法
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if(ack){
                log.info("消息发送成功,消息ID:{}", correlationData.getId());
            }else {
                log.info("消息发送失败, 消息ID:{}, 失败原因:{}", correlationData.getId(), cause);
            }
        });

        String uuid = UUID.randomUUID().toString();
        log.info("发送消息,ID:{}", uuid);

        rabbitTemplate.convertAndSend( "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));
    }
}

在这里我们没有指定消息发送到的交换机,发送到 RabbitMQ 默认的名称为 amq.direct 的直流交换机。

使用postman测试接口:

img

可以看到回调方法被调用,消息成功达到交换机。

发送失败测试:

我们发送消息时指定一个不存在的交换机,模拟发送失败的情况

rabbitTemplate.convertAndSend("not-exists-exchange", "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));

img

由于 RabbitMQ 服务器找不到对应的交换机,返回确认失败的信息(ack = false)。

tips:到目前为止我们还没有创建队列,以上进行的测试只是生产者发送消息到交换机的过程,只要消息成功到达交换机,无论是否有队列可以接收消息,都会返回成功确认信息。

2. 路由消息时导致的消息丢失

当消息到达交换机后,没有相应的队列与交换机绑定就会导致消息丢失。

添加配置:

# 当消息匹配到Queue并且失败时,会通过回调returnCallback方法返回消息
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true

设置找不到匹配队列时的回调方法:

rabbitTemplate.setReturnCallback((message, replyCode, replyText,
                                          exchange, routingKey) -> {
            log.info("return message: {}", new String(message.getBody()));
        });

发送消息:

public void sendMessage(){

        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if(ack){
                log.info("消息发送成功,消息ID:{}", correlationData.getId());
            }else {
                log.info("消息发送失败, 消息ID:{}, 失败原因:{}", correlationData.getId(), cause);
            }
        });

        rabbitTemplate.setReturnCallback((message, replyCode, replyText,
                                          exchange, routingKey) -> {
            log.info("消息路由失败: {}", new String(message.getBody()));
        });

        String uuid = UUID.randomUUID().toString();
        log.info("发送消息,ID:{}", uuid);

        rabbitTemplate.convertAndSend( "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));
  
    }

此时我们还没有创建绑定队列,交换机无法将消息投递到队列

img

图中可以看到,消息成功到达交换机,确认方法被回调,消息成功确认。另一方面,因为找不到对应的绑定队列,ReturnCallback 方法也被回调,表示消息路由失败。

3. 消费消息时导致消息丢失

默认情况下,消费者从队列中获取到消息后会直接确认签收,即自动签收,签收确认后消息会从队列中移除。倘若在消费者在处理收到的消息时发生宕机或者发生程序异常,就会导致消息的丢失。

消费者添加配置:

# 开启消费者手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

在 RabbitMQ 控制台创建名为"test-queue"的队列,并绑定到已经创建的"test-exchange"交换机,绑定键为"test-routing-key"。

img

消费者代码:

@Service
@Slf4j
@RabbitListener(queues = "test-queue")
public class RabbitConsumer {
    /**
     * 指定消费的队列
     */
    @RabbitHandler
    public void consume(String msg, Message message, Channel channel) throws IOException {
        log.info("收到消息: {}", msg);
        // 利用除0运行时异常模拟程序异常
        int i = 2 / 0;
        // 处理完毕,手动确认
        log.info("处理完毕,发送确认");
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

这里我们利用除0运行时异常来模拟程序异常

启动生产者和消费者,postman调用发送消息的接口发送消息,消费者抛出运行时异常,通过控制台查看队列中的消息,如下:

img

图中可以看到消息仍然在队列中,没有被删除。

我们将除0的代码删除,使消息可以正常确认,重新启动消费者,可以看到消息被重新消费

img

再次查看队列,显示消息已经被删除了

img

4. 消息未持久化导致的消息丢失

RabbitMQ提供了交换机、队列、消息的持久化机制。当RabbitMQ宕机时保证数据不会丢失。

4.1 交换机和队列的持久化

我们创建的交换机和队列如何没有配置持久化,RabbitMQ宕机后这些交换机和队列就会丢失,导致消息无法正确地投递。

在 Spring Boot 整合 RabbitMQ 中,我们可以使用配置类创建交换机和队列,如下图:

这两个构造方法只传入名称,默认是持久化的。

4.2 消息的持久化

在 Spring Boot 中使用 rabbitTemplate 发送的消息是默认持久化的,因为其默认设置 delivery_model = 2 。

tips:

  • 由于消息是依附于队列存在的,所以消息持久化必须首先要设置队列的持久化。
  • 由于消息从内存写入磁盘需要时间,如果在此期间发生宕机,未来得及写入磁盘的消息会丢失。

持久化总结:

  1. 同时设置了队列和消息的持久化,RabbitMQ 服务重启后,队列和消息仍然存在。
  2. 仅设置队列的持久化,RabbitMQ 服务重启后,队列存在,队列中的消息会丢失。
  3. 仅设置消息的持久化,RabbitMQ 服务重启后, 由于队列消失,消息也就丢失了。
posted @ 2020-12-10 10:55  starst  阅读(115)  评论(0编辑  收藏  举报