RabbitMQ 消息投递以及ACK机制

RabbitMQ 消息投递以及ACK机制

项目地址

https://gitee.com/Sir-yuChen/website.git

投递出现问题

  • 生产者弄丢了数据
  • RabbitMQ 自己丢了数据
  • 消费端弄丢了数据

生产者丢失数据

生产者将数据发送到 RabbitMQ 的时候,可能在传输过程中因为网络等问题而将数据弄丢了

方案一:开启MQ事务

  • 也就是Transaction事务模式在创建channel的时候,可以把信道设置成事务模式,然后就可以发布消息给RabbitMQ了。如果channel.txCommit();的方法调用成功,就说明事务提交成功,则消息一定到达了RabbitMQ中。

  • 如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,然后进行回滚。

  • 事务模式里面,只有收到了服务的的Commit-ok的指令,才能提交成功。所以可以解决生产者和服务端确认的问题

  • 缺点:事务模式有一个缺点,他是阻塞的,一条消息没有发送完毕,不能发送下一条消息,严重降低RabbitMQ服务器的性能,吞吐量,一般不使用

image-20220322152605943

测试代码:

springboot+RabbitMQ配置类:

rabbitTemplate.setChannelTransacted(true)

image-20220322153422686


// 开启事务
channel.txSelect
try {
    // 这里发送消息
} catch (Exception e) {
    channel.txRollback
 
    // 这里再次重发这条消息
}
 
// 提交事务
channel.txCommit

方案二:Confirm确认模式

生产者设置开启了 confirm 模式之后,每次写的消息都会分配一个唯一的 ID,然后如何写入了 RabbitMQ 之中,RabbitMQ 会给你回传一个 ack 消息,告诉你这个消息发送 OK 了;如果RabbitMQ 没能处理这个消息,会回调你一个 nack 接口,告诉你这个消息失败了,你可以进行重试。

而且你可以结合这个机制知道自己在内存里维护每个消息的 ID,如果超过一定时间还没接收到这个消息的回调,那么你可以进行手动重发。比如:像保证消息的幂等性一样,在 Redis 中存入消息的唯一性ID,只有在成功接收到 ack 消息以后才会删除,否则会定时重发

Confirm确认模式确认机制有三种

  • 单条普通模式
  • 批量确认模式
  • 异步手动确认模式

1. 单条普通模式

在生产者这边通过调用channel.confirmSelect()方法将信道设置为Confirm模式,然后发送消息。一旦消息被投递到交换机之后,RabbitMQ就会发送一个确认(Basic.ACK)给生产者,也就是调用channel.waitForConfirms()返回true,这样生产者就知道消息被服务端接受了

如果网络错误,会抛出连接异常。如果交换机不存在,会抛出404错误

单条普通模式效率不高,并不常用

 	【详细测试代码,在下面】
	//开启发布确认
        channel.confirmSelect();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            channel.basicPublish("", QUEUE_ONE, null, message.getBytes());
            //服务端返回 false 或超时时间内未返回,生产者可以消息重发
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发送成功");
            }
        }

image-20220322163821519

2. 批量确认模式

批量确认,就是在开启confirm模式后,先发送一批消息

  		//开启发布确认
        channel.confirmSelect();
        //批量确认消息大小
        int batchSize = 88;
        //未确认消息个数
        int outstandingMessageCount = 0;
        long begin = System.currentTimeMillis();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            channel.basicPublish("", QUEUE, null, message.getBytes());
            outstandingMessageCount++;
            if (outstandingMessageCount == batchSize) {
                channel.waitForConfirms();//确认代码
                outstandingMessageCount = 0;
            }
        }
        //为了确保还有剩余没有确认消息 再次确认
        if (outstandingMessageCount > 0) {
            channel.waitForConfirms();
        }

image-20220322164117875

  • 只要channel.waitForConfirmsOrDie();方法没有抛出异常,就代表消息被服务端接受了。

  • 批量确认的方式比单条确认的方式效率要高,但是也有两个问题:

  • 问题一:首先就是批量的数量确定。对于不同的业务,到底发送多少条消息确认一次?数量太少,效率提升不上去。数量多的话

  • 问题二:比如我们发1000条消息才确认一次,如果前面999条消息都被接受了,但是最后一条失败了,那么前面的所有数据都需要重发

3. 异步确认模式

异步确认模式需要添加一个confirmListener,并且用一个SortedSet来维护一个批次中没有被确认的消息。

  @Test
    public  void publishMessageAsync() throws Exception {
        try (Channel channel = this.getConnectionMQ()) {
            channel.queueDeclare(QUEUE, false, false, false, null);
            //开启发布确认
            channel.confirmSelect();
            /**
             * 线程安全有序的一个哈希表,适用于高并发的情况
             * 1.轻松的将序号与消息进行关联
             * 2.轻松批量删除条目 只要给到序列号
             * 3.支持并发访问
             */
            ConcurrentSkipListMap<Long, String> outstandingConfirms = new
                    ConcurrentSkipListMap<>();
            /**
             * 确认收到消息的一个回调
             * 1.消息序列号
             * 2.true 可以确认小于等于当前序列号的消息
             * false 确认当前序列号消息
             */
            ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
                if (multiple) {
                    //返回的是小于等于当前序列号的未确认消息 是一个 map
                    ConcurrentNavigableMap<Long, String> confirmed =
                            outstandingConfirms.headMap(sequenceNumber, true);
                    //清除该部分未确认消息
                    confirmed.clear();
                }else{
                    //只清除当前序列号的消息
                    outstandingConfirms.remove(sequenceNumber);
                }
            };
            ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
                String message = outstandingConfirms.get(sequenceNumber);
                System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
            };
            /**
             * 添加一个异步确认的监听器
             * 1.确认收到消息的回调
             * 2.未收到消息的回调
             */
            channel.addConfirmListener(ackCallback, null);
            long begin = System.currentTimeMillis();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = "消息" + i;
                /**
                 * channel.getNextPublishSeqNo()获取下一个消息的序列号
                 * 通过序列号与消息体进行一个关联
                 * 全部都是未确认的消息体
                 */
                outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
                channel.basicPublish("", QUEUE, null, message.getBytes());
            }
            long end = System.currentTimeMillis();
            //发布888个异步确认消息,耗时28ms
            System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) +
                    "ms");
        }
    }

注意:

  • 在Spring boot +rabbitMq 项目confirm模式实在channel中开启的,RabbitTemplate对channel进行了封装

    rabbitTemplate.setConfirmCallback(自己的实现类)//实现RabbitTemplate.ConfirmCallback
    

    测试完整代码:

    package com.zy.website.test.mq;
    
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.ConfirmCallback;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import com.zy.website.test.BaseTest;
    import org.junit.Test;
    
    import java.util.concurrent.ConcurrentNavigableMap;
    import java.util.concurrent.ConcurrentSkipListMap;
    
    /**
     * 测试MQ Confirm确认机制
     **/
    public class MqConfirmTest extends BaseTest {
        //设置执行次数
        public static final int MESSAGE_COUNT = 888;
        private static final String QUEUE = "confirmTestQueue";
    
        public Channel getConnectionMQ() throws Exception {
            ConnectionFactory factory = new ConnectionFactory();
            //设置MabbitMQ所在服务器的ip和端口
            factory.setHost("127.0.0.1");
            factory.setPort(5672);
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            return channel;
        }
        /**
         * 单个发布确认
         */
        @Test
        public void publishMessageIndividually() throws Exception {
            Channel channel = this.getConnectionMQ();
            //声明队列
            channel.queueDeclare(QUEUE, false, false, false, null);
            //开启发布确认
            channel.confirmSelect();
            long begin = System.currentTimeMillis();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = i + "";
                channel.basicPublish("", QUEUE, null, message.getBytes());
                //服务端返回 false 或超时时间内未返回,生产者可以消息重发
                boolean flag = channel.waitForConfirms();
                if (flag) {
                    System.out.println("消息发送成功" + i);
                }
            }
            long end = System.currentTimeMillis();
            //发布888个单独确认消息,耗时526ms
            System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
                    "ms");
        }
        /**
         * 批量发布确认
         */
        @Test
        public void publishMessageBatch() throws Exception {
            Channel channel = this.getConnectionMQ();
            channel.queueDeclare(QUEUE, false, false, false, null);
            //开启发布确认
            channel.confirmSelect();
            //批量确认消息大小
            int batchSize = 88;
            //未确认消息个数
            int outstandingMessageCount = 0;
            long begin = System.currentTimeMillis();
            for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = i + "";
                channel.basicPublish("", QUEUE, null, message.getBytes());
                outstandingMessageCount++;
                if (outstandingMessageCount == batchSize) {
                    channel.waitForConfirms();//确认代码
                    outstandingMessageCount = 0;
                }
            }
            //为了确保还有剩余没有确认消息 再次确认
            if (outstandingMessageCount > 0) {
                channel.waitForConfirms();
            }
            long end = System.currentTimeMillis();
            System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) +
                    "ms");
        }
        /**
         * 异步发布确认
         */
        @Test
        public void publishMessageAsync() throws Exception {
            try (Channel channel = this.getConnectionMQ()) {
                channel.queueDeclare(QUEUE, false, false, false, null);
                //开启发布确认
                channel.confirmSelect();
                /**
                 * 线程安全有序的一个哈希表,适用于高并发的情况
                 * 1.轻松的将序号与消息进行关联
                 * 2.轻松批量删除条目 只要给到序列号
                 * 3.支持并发访问
                 */
                ConcurrentSkipListMap<Long, String> outstandingConfirms = new
                        ConcurrentSkipListMap<>();
                /**
                 * 确认收到消息的一个回调
                 * 1.消息序列号
                 * 2.true 可以确认小于等于当前序列号的消息
                 * false 确认当前序列号消息
                 */
                ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
                    if (multiple) {
                        //返回的是小于等于当前序列号的未确认消息 是一个 map
                        ConcurrentNavigableMap<Long, String> confirmed =
                                outstandingConfirms.headMap(sequenceNumber, true);
                        //清除该部分未确认消息
                        confirmed.clear();
                    } else {
                        //只清除当前序列号的消息
                        outstandingConfirms.remove(sequenceNumber);
                    }
                };
                ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
                    String message = outstandingConfirms.get(sequenceNumber);
                    System.out.println("发布的消息" + message + "未被确认,序列号" + sequenceNumber);
                };
                /**
                 * 添加一个异步确认的监听器
                 * 1.确认收到消息的回调
                 * 2.未收到消息的回调
                 */
                channel.addConfirmListener(ackCallback, null);
                long begin = System.currentTimeMillis();
                for (int i = 0; i < MESSAGE_COUNT; i++) {
                    String message = "消息" + i;
                    /**
                     * channel.getNextPublishSeqNo()获取下一个消息的序列号
                     * 通过序列号与消息体进行一个关联
                     * 全部都是未确认的消息体
                     */
                    outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
                    channel.basicPublish("", QUEUE, null, message.getBytes());
                }
                long end = System.currentTimeMillis();
                //发布888个异步确认消息,耗时28ms
                System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) +
                        "ms");
            }
        }
    }
    
    

RabbitMq自己丢失数据

消息从交换机路由到队列

  • 当前队列不存在,消息丢失
  • routinKey错误,消息丢失

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

测试:

  1. 发送一条消息到一个不存在的routinKey
  2. 触发回发机制

配置类:

    rabbitTemplate.setMandatory(true);
    rabbitTemplate.setReturnCallback(new MqReturnCallBack());//消息回发

image-20220322170806010

MqReturnCallBack类:


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

/**
 * MQ 消息回发
 **/
@Component
public class MqReturnCallBack implements RabbitTemplate.ReturnCallback {

    private static Logger logger = LogManager.getLogger(MqConfirmCallback.class);

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        try {
            logger.info("MQ消息回发  return--message:" + new String(message.getBody(), "UTF-8") + ",replyCode:" + replyCode
                    + ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
        } catch (UnsupportedEncodingException e) {
            logger.error("MQ消息回发 异常{}",e);
        }
    }
}

发送消息:

image-20220322170853824

打印日志:

image-20220322170912465

备份交换机

在创建交换机的时候,从属性中指定备份交换机即可

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

持久化

RabbitMQ的服务或者硬件发生故障,比如系统宕机、重启、关闭等,可能会导致内存中的消息丢失,所以我们要把消息本身和元数据(队列、交换机、绑定信息)都保存到磁盘中

1. 队列持久化

​ queue的持久化是通过durable=true来实现的,只需要在队列声明的时候设置

注意:只会持久化队列,并不会持久化队列中的消息

image-20220322172025175

//源码
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete) {
        this(name, durable, exclusive, autoDelete, (Map)null);
    }
//参数:
/*
queue:queue的名称
exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对首次申明它的连接可见,并在连接断开时自动删除。
    这里需要注意三点:
    1. 排他队列是基于连接可见的,同一连接的不同信道是可以同时访问同一连接创建的排他队列;
    2.“首次”,如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;
    3.即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的,这种队列适用于一个客户端发送读取消息的应用场景。
autoDelete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。
*/

2. 消息持久化

设置消息的持久化:

image-20220322172825062

总要参数为:setDeliveryMode()发送消息的时候会确认消息是否进行持久化

枚举类详情MessageDeliveryMode:
public enum MessageDeliveryMode {
    NON_PERSISTENT,
    PERSISTENT;

    private MessageDeliveryMode() {
    }

    public static int toInt(MessageDeliveryMode mode) {
        switch(mode) {
        case NON_PERSISTENT:
            return 1;
        case PERSISTENT:
                //当返回为2的时候就会进行消息的持久化
            return 2;
        default:
            return -1;
        }
    }

    public static MessageDeliveryMode fromInt(int modeAsNumber) {
        switch(modeAsNumber) {
        case 1:
            return NON_PERSISTENT;
        case 2:
            return PERSISTENT;
        default:
            return null;
        }
    }
}

    

3. 交换机持久化

如果exchange不设置持久化,那么当broker服务重启之后,exchange将不复存在,那么既而发送方rabbitmq producer就无法正常发送消息

image-20220322173212834


消费者端丢失数据

手动ACK及自动ACK

  • 为了保证消息从队列可靠的达到消费者,RabbitMQ 提供了消息确认机制(Message Acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 参数等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移除消息(实际上是先打上删除标记,之后在删除)。当 autoAck 参数等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。

  • 采用消息确认机制后,只要设置 autoAck 参数为 false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。

  • 当autoAck 参数为 false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 服务器端一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

  • RabbitMQ 不会为未确认的消息设置过期时间,判断此消息是否需要重新投递给消费者的唯一依据是消费该消息连接是否已经断开,这个设置的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久

  • RabbitMQ 的 Web 管理平台上可以看到当前队列中的 “Ready” 状态和 “Unacknowledged” 状态的消息数,分别对应等待投递给消费者的消息数和已经投递给消费者但是未收到确认信号的消息数

image-20220322173629493

  • ACK分为手动和自动
    • 消费者确认发生在监听队列的消费者处理业务失败,如:发生了异常,不符合要求的数据等,这些场景我们就需要手动处理,比如重新发送或者丢弃
    • RabbitMQ 消息确认机制(ACK)默认是自动确认的,自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,假如你用回滚了也只是保证了数据的一致性,但是消息还是丢了,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
  • 消息确认模式:
    • AcknowledgeMode.NONE:自动确认
    • AcknowledgeMode.AUTO:根据情况确认
    • AcknowledgeMode.MANUAL:手动确认
  • 消费者收到消息后,手动调用 Basic.Ack 或 Basic.Nack 或 Basic.Reject 后,RabbitMQ 收到这些消息后,才认为本次投递完成
    • Basic.Ack 命令:用于确认当前消息
    • Basic.Nack 命令:用于否定当前消息(注意:这是AMQP 0-9-1的RabbitMQ扩展)
    • Basic.Reject 命令:用于拒绝当前消息

1. basicAck 方法

basicAck 方法用于确认当前消息

void basicAck(long var1, boolean var3) throws IOException;
/*
参数说明:
long deliveryTag:唯一标识 ID。

boolean multiple:上面已经解释。

boolean requeue:
	如果 requeue 参数设置为 true,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
	如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,而不会把它发送给新的消费者。
*/

image-20220322174237871

2. basicNack 方法

basicNack 方法用于否定当前消息。 由于 basicReject 方法一次只能拒绝一条消息,如果想批量拒绝消息,则可以使用 basicNack 方法。消费者客户端可以使用 channel.basicNack 方法来实现,方法定义如下:

    void basicNack(long var1, boolean var3, boolean var4) throws IOException;
/*
参数说明:
long var1:唯一标识 ID。

boolean var3:上面已经解释。

boolean var4:
	如果 requeue 参数设置为 true,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
    如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,而不会把它发送给新的消费者
*/

**3.basicReject 方法 **

basicReject 方法用于明确拒绝当前的消息而不是确认。 RabbitMQ 在 2.0.0 版本开始引入 Basic.Reject 命令,消费者客户端可以调用与其对应的 channel.basicReject 方法来告诉 RabbitMQ 拒绝这个消息

void basicReject(long deliveryTag, boolean requeue) throws IOException;
/*
参数说明:
long deliveryTag:唯一标识 ID。

boolean requeue:上面已经解释
*/

image-20220322174443793

4. basicRecover方法

是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己

RecoverOk basicRecover(boolean var1) throws IOException;

RabbitMQ学习系列

RabbitMQ 安装快速下载

springboot简单整合RabbitMQ

RabbitMQ消息队列

RabbitMQ 消息投递以及ACK机制

RabbitMQ 消息幂等性&顺序性&消息积压&面试问题

posted @ 2022-03-23 11:35  Mr*宇晨  阅读(2193)  评论(0编辑  收藏  举报