RabbitMQ学习(四)之 “消息队列可靠性投递”

1. 消息可靠性投递

前言

在代码里面一定是先操作数据库再发送消息。避免因为数据库回滚导致的数据不一致。但是如果先操作数据,后发送消息,发送消息出了问题,那不是一样会出现业务数据的不一致?

这篇文章我们来分析 RabbitMQ 的可靠性投递,也就是在使用 RabbitMQ 实现异步通信的时候,消息丢了怎么办,消息重复消费怎么办?在 RabbitMQ 里面提供了很多保证消息可靠投递的机制,这个也是 RabbitMQ 的一个特性。

在学习RabbitMQ前,必须要明确一个问题,因为效率与可靠性是无法兼得的,如果要保证每一个环节都成功,势必会对消息的收发效率造成影响。所以如果是一些业务实时一致性要求不是特别高的场合,可以牺牲一些可靠性来换取效率。比如发送通知或者记录日志的这种场景,如果用户没有收到通知,不会造成业务影响,只要再次发送就可以了。

在我们使用 RabbitMQ 收发消息的时候,有几个主要环节:
在这里插入图片描述

  1. 1 代表消息从生产者发送到 Broker。
    生产者把消息发到 Broker 之后,怎么知道自己的消息有没有被 Broker 成功接收?
  2. 2 代表消息从 Exchange 路由到 Queue
    Exchange 是一个绑定列表,如果消息没有办法路由到正确的队列,会发生什么事情?应该怎么处理?
  3. 3 代表消息在 Queue 中存储
    队列是一个独立运行的服务,有自己的数据库(Mnesia),它是真正用来存储消息的。如果还没有消费者来消费,那么消息要一直存储在队列里面。如果队列出了问题,消息肯定会丢失。怎么保证消息在队列稳定地存储呢?
  4. 4 代表消费者订阅 Queue 并消费消息
    队列的特性是什么?FIFO。队列里面的消息是一条一条的投递的,也就是说,只有上一条消息被消费者接收以后,才能把这一条消息从数据库删掉,继续投递下一条消息。那么问题来了,Broker 怎么知道消费者已经接收了消息呢?

解决MQ可靠性投递,主要就是解决上面四个环节带来的问题

下面有9种层面去解决问题

1. 消息发送到 RabbitMQ 服务器

第一个环节是生产者发送消息到 Broker。可能因为网络或者 Broker 的问题导致消息发送失败,生产者不能确定 Broker 有没有正确的接收。在 RabbitMQ 里面提供了两种机制服务端确认机制,也就是在生产者发送消息给RabbitMQ 的服务端的时候,服务端会通过某种方式返回一个应答,只要生产者收到了这个应答,就知道消息发送成功了。
第一种是 Transaction(事务)模式,第二种 Confirm(确认)模式。

  • Transaction (事务)模式
    事务模式如何使用呢?
    我们通过一个 channel.txSelect() 的方法把信道设置成事务模式,然后就可以发布消
    息给 RabbitMQ 了,如果 channel.txCommit();的方法调用成功,就说明事务提交成功,则消息一定到达了 RabbitMQ 中。
    如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,这个时
    候我们便可以将其捕获,进而通过执行 **channel.txRollback()**方法来实现事务回滚。

AMQP 协议抓包示意图:
在这里插入图片描述
在事务模式里面,只有收到了服务端的 Commit-OK 的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干 RabbitMQ 服务器的性能。所以不建议在生产环境使用。
Spring Boot 中的设置:

rabbitTemplate.setChannelTransacted(true);
  • Confirm ( 确认 ) 模式
    可以保证消息被 Broker 接收,但是又不大量消耗性能的方式
    确认模式有三种
  1. 普通确认模式
    在生产者这边通过调用 **channel.confirmSelect()**方法将信道设置为 Confirm 模式,然后发送消息。一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(Basic.Ack)给生产者,也就是调用 **channel.waitForConfirms()**返回 true,这样生产者就知道消息被服务端接收了。
  2. 批量确认的方式
    批量确认,就是在开启 Confirm 模式后 ,先发送一批消息 。 只要channel.waitForConfirmsOrDie();方法没有抛出异常,就代表消息都被服务端接收了。批量确认的方式比单条确认的方式效率要高。
    但是也有两个问题,第一个就是批量的数量的确定。对于不同的业务,到底发送多少条消息确认一次?数量太少,效率提升不上去。数量多的话,又会带来另一个问题,比如我们发 1000 条消息才确认一次,如果前面 999 条消息都被服务端接收了,如果第 1000 条消息被拒绝了,那么前面所有的消
    息都要重发。
  3. 异步确认模式
    异步确认模式需要添加一个 ConfirmListener,并且用一个 SortedSet 来维护没有被确认的消息。Confirm 模式是在 Channel 上开启的,因为 RabbitTemplate 对 Channel 进行了封装,叫做 ConfimrCallback。
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    
    
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
		if (!ack) {
    
    
			System.out.println("发送消息失败:" + cause);
			throw new RuntimeException("发送异常:" + cause);
		}
	}
})

2. 消息从交换机路由到队列

第二个环节就是消息从交换机路由到队列。在什么情况下,消息会无法路由到正确的队列?可能因为路由键错误,或者队列不存在。

我们有两种方式处理无法路由的消息,一种就是让服务端回发给生产者,一种是让交换机路由到另一个备份的交换机。

  • 消息回发
    使用 mandatory 参数和 ReturnListener(在 Spring AMQP 中是ReturnCallback)。
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
    
    
        public void returnedMessage (Message message,
        int replyCode,
        String replyText,
        String exchange,
        String routingKey){
    
    
        System.out.println("回发的消息:");
        System.out.println("replyCode: " + replyCode);
        System.out.println("replyText: " + replyText);
        System.out.println("exchange: " + exchange);
        System.out.println("routingKey: " + routingKey);
    }
});
  • 消息路由到备份交换机
    在创建交换机的时候,从属性中指定备份交换机。
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE"); // 指定交换机的备份交换机
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

注意区别,队列可以指定死信交换机;交换机可以指定备份交换机

3. 消息在队列中存储

第三个环节是消息在队列存储,如果没有消费者的话,队列一直存在在数据库中。如果 RabbitMQ 的服务或者硬件发生故障,比如系统宕机、重启、关闭等等,可能会导致内存中的消息丢失,所以我们要把消息本身和元数据(队列、交换机、绑定)都保存到磁盘(持久化存储)。
解决方案有:

  1. 队列持久化
@Bean("Queue")
public Queue Queue() {
    
    
	// queueName, durable, exclusive, autoDelete, Properties
	return new Queue("TEST_QUEUE", true, false, false, new HashMap<>());
}
  1. 交换机持久化
@Bean("Exchange")
public DirectExchange exchange() {
    
    
	// exchangeName, durable, exclusive, autoDelete, Properties
	return new DirectExchange("TEST_EXCHANGE", true, false, new HashMap<>());
}
  1. 消息持久化
MessageProperties messageProperties = new MessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
Message message = new Message("持久化消息".getBytes(), messageProperties);
rabbitTemplate.send("TEST_EXCHANGE", "test", message);
  1. 集群
    如果只有一个 RabbitMQ 的节点,即使交换机、队列、消息做了持久化,如果服务崩溃或者硬件发生故障,RabbitMQ 的服务一样是不可用的,所以为了提高 MQ 服务的可用性,保障消息的传输,我们需要有多个 RabbitMQ 的节点

4. 消息投递到消费者

如果消费者收到消息后没来得及处理即发生异常,或者处理过程中发生异常,会导致④失败。服务端应该以某种方式得知消费者对消息的接收情况,并决定是否重新投递这条消息给其他消费者。

RabbitMQ 提供了消费者的消息确认机制(message acknowledgement),消费者可以自动或者手动地发送 ACK 给服务端。没有收到 ACK 的消息,消费者断开连接后,RabbitMQ 会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑。

消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式地回复确认信号后才从队列中移去消息。

如何设置手动 ACK?
SimpleRabbitListenerContainer 或者 SimpleRabbitListenerContainerFactory

factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

application.properties

spring.rabbitmq.listener.direct.acknowledge-mode=manual 
spring.rabbitmq.listener.simple.acknowledge-mode=manual

注意这三个值的区别:

  • NONE:自动 ACK
  • MANUAL: 手动 ACK
  • AUTO:如果方法未抛出异常,则发送 ack。

生产者最终确定消费者有没有消费成功的两种方式:

  1. 消费者收到消息,处理完毕后,调用生产者的 API(思考:是否破坏解耦?)
  2. 消费者收到消息,处理完毕后,发送一条响应消息给生产者

其他保证可靠性方式

1. 消费者回调

  1. 调用生产者 API,修改数据状态
  2. 发送响应消息给生产者

2. 补偿机制

如果生产者的 API 就是没有被调用,也没有收到消费者的响应消息,怎么办? 不要着急,可能是消费者处理时间太长或者网络超时。 生产者与消费者之间应该约定一个超时时间,比如 5 分钟,对于超出这个时间没有得到响应的消息,可以设置一个定时重发的机制,但要发送间隔和控制次数,比如每隔 2 分钟发送一次,最多重发 3 次,否则会造成消息堆积。

重发可以通过消息落库+定时任务来实现。
重发,是否发送一模一样的消息?

3. 消息幂等性

如果消费者每一次接收生产者的消息都成功了,只是在响应或者调用 API 的时候出了问题,会不会出现消息的重复处理?例如:存款 100 元,ATM 重发了 5 次,核心系统一共处理了 6 次,余额增加了 600 元。
所以,为了避免相同消息的重复处理,必须要采取一定的措施。RabbitMQ 服务端是没有这种控制的(同一批的消息有个递增的 DeliveryTag),它不知道你是不是就要把一条消息发送两次,只能在消费端控制。
如何避免消息的重复消费?消息出现重复可能会有三个原因:

  1. 生产者的问题,环节1重复发送消息,比如在开启了 Confirm 模式但未收到确认,生产者者重复投递。
  2. 环节4出了问题,由于消费者未发送 ACK 或者其他原因,消息重复投递。
  3. 生产者代码或者网络问题。

对于重复发送的消息,可以对每一条消息生成一个唯一的业务 ID,通过日志或者消息落库来做重复控制。

2.消息的顺序性

消息的顺序性指的是消费者消费消息的顺序跟生产者生产消息的顺序是一致的。

例如:1、发表微博;2、发表评论;3、删除微博。顺序不能颠倒。
在 RabbitMQ 中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)。

猜你喜欢

转载自blog.csdn.net/nonage_bread/article/details/111416320