RabbitMQ消息可靠性投递分析

1、消息可靠性投递分析

由RabbitMQ消息发送的过程,我们可以知道如果要保证消息的可靠性投递,必须要下保证如下图的4个环节,消息的可靠性

  1. Producer发送消息到Broker。
  2. 消息从Exchange路由到Queue。
  3. 消息在Queue中的持久化存储。
  4. Consumer订阅并消费消息。

根据以上的4个环节,我们来逐一分析。

1.1、Producer发送消息到Broker

在Producer发送消息到Broker的过程中,如果Broker由于网络或者磁盘问题,未能成功接收消息,这时候Producer如何知道消息是否成功发送给Broker呢?

所以这里RabbitMQ提供了服务端的确认机制,只有Producer收到Broker的消息确认,才能说明消息被成功接收。这种确认机制有如下两种:

  • **Transaction(事务)模式 **
  • Confirm(确认)模式

1.1.1、Transaction(事务)模式

这里是事务和我正常理解的事务,其实就是一个道理,在消息发送过程中出现失败的情况,会回滚消息。

通过WireShark抓包,我们可以知道。使用事务模式的时候,Producer和Broker的交互如下图所示:

java API 如下:

代码示例

try {

    channel.txSelect();// 开始事务模式
    channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
    channel.txCommit();// 提交
    System.out.println("消息发送成功");
} catch (Exception e) {
    channel.txRollback();// 回滚
    System.out.println("消息已经回滚");
}

Springboot 如下

TemplateConfig中配置

代码示例

rabbitTemplate.setChannelTransacted(true):

在事务模式里面,只有收到了服务端的Commit-OK的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干RabbitMQ服务器的性能。所以不建议大家在生产环境使用。

1.1.2、Confirm(确认)模式

1.1.2.1、普通确认模式

hannel.confirmSelect()开启确认模式,Broker接收到消息会给一个Basic.Ack,Producer这边通过channel.waitForConfirms()接收回执。

代码示例

// 开启发送方确认模式
channel.confirmSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// 普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
    System.out.println("消息发送成功" );
}else{
    System.out.println("消息发送失败");
}
1.1.2.2、批量确认模式

hannel.confirmSelect()开启确认模式,Broker接收到所有的消息才会给一个Basic.Ack,Producer这边通过channel.waitForConfirmsOrDie()接收回执。只要waitForConfirmsOrDie没有抛出异常,就代表服务端接收成功。这种方式提高了效率。但是,这里批量发送的消息如果有一条出现问题,所有的消息都发送不成功。

代码示例

try {
    channel.confirmSelect();
    for (int i = 0; i < 5; i++) {
        // 发送消息
        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    }
    // 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
    // 比如5条消息可能只收到1个ACK,也可能收到2个(抓包才看得到)
    // 直到所有信息都发布,只要有一个未被Broker确认就会IOException
    channel.waitForConfirmsOrDie();
    System.out.println("消息发送完毕,批量确认成功");
} catch (Exception e) {
    // 发生异常,可能需要对所有消息进行重发
    e.printStackTrace();
}
1.1.2.3、异步确认模式

异步确认,顾名思义就是发送和确认消息不是同步的。这样就可以一边发送消息,一边确认消息。通过confirmSelect开启确认模式,这里通过添加ConfirmListener监听来异步确认消息。其中handleAck方法是服务端已经确认的消息回调;handleNack方法是服务为确认消息回调。

代码示例

// 用来维护未确认消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

// 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
// 异步监听确认和未确认的消息
// 如果要重复运行,先停掉之前的生产者,清空队列
channel.addConfirmListener(new ConfirmListener() {
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Broker未确认消息,标识:" + deliveryTag);
        if (multiple) {
            // headSet表示后面参数之前的所有元素,全部删除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
        // 这里添加重发的方法
    }
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        // 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
        System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
        if (multiple) {
            // headSet表示后面参数之前的所有元素,全部删除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            // 只移除一个元素
            confirmSet.remove(deliveryTag);
        }
        System.out.println("未确认的消息:"+confirmSet);
    }
});

// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
    long nextSeqNo = channel.getNextPublishSeqNo();
    // 发送消息
    // String exchange, String routingKey, BasicProperties props, byte[] body
    channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);

// 这里注释掉的原因是如果先关闭了,可能收不到后面的ACK
//channel.close();
//conn.close();

Springboot中示例

TemplateConfig中进行设置。代码示例

rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            System.out.println("发送消息失败:" + cause);
            throw new RuntimeException("发送异常:" + cause);
        }
    }
});

1.2、消息从Exchange路由到Queue

在消息从Exchange路由到Queue的过程中出现问题。可能的情况有如下两种。一是,routing key错误;或队列不存在。

RabbitMQ提供了两种方式来处理:

  • Broker重新发送给Producer(通过Producer设置回调的方式)。
  • 让交换机路由到备份的交换机。

1.2.1、Broker重新发送给Producer

java API,通过设置ReturnListener来实现回调。

代码示例

channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int replyCode,
                             String replyText,
                             String exchange,
                             String routingKey,
                             AMQP.BasicProperties properties,
                             byte[] body)
        throws IOException {
        System.out.println("=========监听器收到了无法路由,被返回的消息============");
        System.out.println("replyText:"+replyText);
        System.out.println("exchange:"+exchange);
        System.out.println("routingKey:"+routingKey);
        System.out.println("message:"+new String(body));
    }
});

SpringBoot中使用,在TemplateConfig中设置,通过设置ReturnCallback来实现回调。

代码示例

 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);
     }
 });

1.2.2、交换机路由到备份的交换机

这里是通过声明交换机的时候,设置备用交换机的参数alternate-exchange,来指定备用交换机。

代码示例

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
    contentEncoding("UTF-8").build();

// 备份交换机
channel.exchangeDeclare("ALTERNATE_EXCHANGE","topic", false, false, false, null);
channel.queueDeclare("ALTERNATE_QUEUE", false, false, false, null);
channel.queueBind("ALTERNATE_QUEUE","ALTERNATE_EXCHANGE","#");

// 在声明交换机的时候指定备份交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

1.3、消息在Queue中的持久化存储

这里我们来学习一些消息存储持久化的设置。

1.3.1、队列(Queue)的持久化

在声明队列的时候注意选择参数durable=true,就代表持久化设置。

// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);

durable:没有持久化的队列,保存在内存中,服务重启后队列和消息都会消失(钱和人一起没了)。

autoDelete:没有消费者连接的时候,自动删除。

exclusive:排他性队列的特点是:

  1. 只对首次声明它的连接(Connection)可见。
  2. 会在其连接断开的时候自动删除。

1.3.2、交换机的持久化

同样是设置如下的几个参数,来实现持久化。durable

// 声明交换机
// String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

1.3.3、消息持久化

消息的持久化,是通过BasicProperties构造,设置deliveryMode=2,代表消息进行了持久化。

// 对每条消息设置过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    .deliveryMode(2) // 持久化消息
    .contentEncoding("UTF-8")
    .expiration("10000") // TTL
    .build();

1.3.4、集群

集群就是通过备份的方式提高RabbitMQ的可用性。

1.4、Consumer消费消息

如果Consumer接收到消息以后,在处理的过程中出现了异常,导致了Comsumer消费消息的失败。在这种情况下,该怎么办呢?

不用慌,RabbitMQ给我们提供了Consumer确认机制(熟悉不熟悉,服务端是不是也有个消息确认机制)。Comsumer收到并处理完消息以后,手动或者自动给服务端一个ACK。

没有收到ACK的消息,消费者断开连接后,RabbitMQ会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑(如果代码修复好了还好)。

消费者确定接受消息的方式有如下两种:

  • 自动确认(ACK)
  • 手动确认(ACK)

1.4.1、自动ACK

自动ACK,这个也是默认的情况。也就是我们没有在消费者处编写ACK的代码,消费者会在收到消息的时候就自动发送ACK,而不是在方法执行完毕的时候发送ACK(并不关心你有没有正常消息)。

1.4.2、手动ACK

可以实现在消费者业务逻辑处理完成后,在进行确认。

1、java api使用
// 第一步、autoAck设置成false,声明自动确认
// String queue, boolean autoAck, Consumer callback
channel.basicConsume(QUEUE_NAME, false, consumer);

// 第二步、在Consumer的回调中,调用channel的如下方法,实现消息确认、拒绝、异常处理
 if (msg.contains("拒收")){
     // 拒绝消息
     // requeue:是否重新入队列,true:是;false:直接丢弃,相当于告诉队列可以直接删除掉
     // TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
     channel.basicReject(envelope.getDeliveryTag(), false);
 } else if (msg.contains("异常")){
     // 批量拒绝
     // requeue:是否重新入队列
     // TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
     channel.basicNack(envelope.getDeliveryTag(), true, false);
 } else {
     // 手工应答
     // 如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费
     channel.basicAck(envelope.getDeliveryTag(), true);
 }

代码示例

2、springboot使用

首先在application.properties文件中,进行如下配置

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

注意这里有三个选项可以选择

  • none:自动ACK
  • manual:手动ACK
  • auto:如果方法未抛出异常,则发送ack。如果方法抛出异常,并且不是AmqpRejectAndDontRequeueException则发送nack,并且重新入队列。如果抛出异常时AmqpRejectAndDontRequeueException则发送nack不会重新入队列。

然后再如下消费方法中进行确认

@RabbitListener(queues = "${com.fanger.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
    @RabbitHandler
    public void process(String msgContent,Channel channel, Message message) throws IOException {
        System.out.println("Second Queue received msg : " + msgContent );
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // 确认
//        channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,false); // 批量拒绝
//        channel.basicReject(message.getMessageProperties().getDeliveryTag(),false); // 拒绝
    }
}

1.5、消费者回调

处理以上4个环节,保证消息可靠投递,我们还可以通过消费者执行成功后,给生成这一回执的方式来确保消息可靠。这里生产者可以提供响应的API来接收通知。

1.6、消息补偿机制

如果,消费者由于种种原因,没有回调生产者API。消息没有被处理成功怎么办呢?

这时候,咱们就需要进行消息补偿,也就重新发送消息。

这里可以通过一张消息的状态表,通过定时任务来,扫描表,然后重发消息。这里要注意的是

  • 重发的次数?
  • 重发消息的时间间隔?

这两个问题,可以根据系统设计而定,不过重试次数别超过3次。

1.7、消息的幂等性

如果有了1.6的消息补偿机制,就一定要保证消息的幂等性。

可能出现重复可能的原因:

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

如何避免消息被重复消费呢?

重复消息,可以通过给每个消息生成唯一的业务ID,然后落库的时候判重来控制。

1.8、消息的顺序性

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

在RabbitMQ中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)。

作者:fanger8848
链接:https://juejin.cn/post/6986572514826649636
来源:掘金

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章