RabbitMQ之持久化不公平分发 预取值发布确认模式

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RabbitMQ之持久化不公平分发 预取值发布确认模式相关的知识,希望对你有一定的参考价值。

一、持久化

1、简介

        刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当RabbitMQ服务停掉以后消息生产者发送过来的消息不丢失。默认情况下RabbitMQ退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化

2、队列持久化

        之前我们创建的队列都是非持久化的,rabbitmq如果重启的化,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把durable参数设置为持久化

 // 声明队列
 // 持久化 需要让Queue持久化
 boolean durable = true;
 channel.queueDeclare(TASK_QUEUE_NAME,durable,false,false,null);

 需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除或者重新创建一个持久化的队列,不然就会出现错误

重新运行代码后,队列持久化成功

3、消息持久化

  • 要想让消息实现持久化需要在消息生产者修改代码,MessageProperties,PERSISTENT_TEXT_PLAIN添加这个属性。
  • 将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考后边发布确认模式
 //设置生产者发送消息为持久化消息(要求保存到磁盘上)
 channel.basicPublish("",TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN
                      ,message.getBytes(StandardCharsets.UTF_8));
 System.out.println("生产者发出消息:"+message);


二、不公平分发

1、简介

        在最开始的时候我们学习到RabbitMQ.分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者1处理任务的速度非常快,而另外一个消费者2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ并不知道这种情况,它依然很公平的进行分发。

2、为了避免这种情况,我们可以设置参数channel.basicQos(1)

// 设置不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);

三、  预取值

1、简介

        本身消息的发送就是异步发送的,所以在任何时候,channel上肯定不止只有一个消息,另外来自消费者的手动确认,本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用basic.gos.方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息5、6、7,8,并且通道的预取计数设置为4,此时RabbitMQ.将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被ack。比方说tag=6这个消息刚刚被确认ACK,RabbitMQ将会感知这个情况到并再发送一条消息。消息应答和QoS预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的RAM消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同100到300范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为1是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。

 

四、发布确认

1、发布确认原理

  • 生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外 broker也可以设置basic.ack 的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
  • confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack.消息。

2、发布确认策略

开启发布确认的方法:

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布角认,都需要在channel上调用该方法

Channel channel = connection.createChannel();
channel.confirmSelect();

3、单个发布确认

  1. 这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布, waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

  2. 这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息颍会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

    /*
 * 发布确认模式,
 * 1、单个确认
 * 2、批量确认
 * 3、异步批量确认
 * */
 public class ComfirmMessage 
 
     // 批量发消息的个数
     public static final int MESSAGE_COUNT = 1000;
 
     public static void main(String[] args) throws Exception 
         // 1、单个确认
         // 发布1000个单独确认消息,耗时567ms
         ComfirmMessage.publishMessageIndividually();
 
     
 
     public static void publishMessageIndividually() throws Exception 
         Channel channel = RabbitMqUtils.getChannel();
         String queueName = UUID.randomUUID().toString();
         channel.queueDeclare(queueName,false,false,false,null);

     // 开启发布确认
     channel.confirmSelect();
     // 开始时间
     long begin = System.currentTimeMillis();

     // 批量发消息
     for (int i = 0; i < MESSAGE_COUNT; i++) 
         String message = i + "";
         channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
         // 单个消息马上进行发布确认
         boolean flag = channel.waitForConfirms();
         if (flag)
             System.out.println("消息发送成功");
         
     

     // 结束时间
     long end = System.currentTimeMillis();
     System.out.println("发布"+MESSAGE_COUNT+"个单独确认消息,耗时"+ (end - begin) + "ms");
 

4、批量确认发布

        上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布

/*
* 发布确认模式,
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
* */
public class ComfirmMessage 

    // 批量发消息的个数
    public static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception 
        //2、批量确认
        // 发布1000个批量确认消息,耗时37ms
        ComfirmMessage.publishMessageBatch();
    

    public static void publishMessageBatch() throws Exception
        Channel channel = RabbitMqUtils.getChannel();
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,false,false,false,null);

        // 开启发布确认
        channel.confirmSelect();
        // 开始时间
        long begin = System.currentTimeMillis();

        // 批量确认消息大小
        int batchSize = 1000;

        // 批量发送 批量确认
        for (int i = 0; i < MESSAGE_COUNT; i++) 
            String message = i + "";
            channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));

            // 判断达到100条消息的时候,批量确认一次
            if (i%batchSize == 0)
                // 确认发布
                channel.waitForConfirms();
            
        

        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布"+MESSAGE_COUNT+"个批量确认消息,耗时"+ (end - begin) + "ms");
    

5、异步发布确认

        异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。

/*
* 发布确认模式,
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
* */
public class ComfirmMessage 

    // 批量发消息的个数
    public static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception 
        //3、异步批量确认
        // 发布1000个异步确认消息,耗时36ms
        ComfirmMessage.publicMessageAsync();

    

    public static void publicMessageAsync() throws Exception
        Channel channel = RabbitMqUtils.getChannel();
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,false,false,false,null);

        // 开启发布确认
        channel.confirmSelect();
        // 开始时间
        long begin = System.currentTimeMillis();

        // 消息确认成功回调函数
        ConfirmCallback ackCallback = (deliveryTag,multiply) -> 
            System.out.println("确认的消息:"+deliveryTag);
        ;

        // 消息确认失败回调函数
        /*
        * 参数1:消息的标记
        * 参数2:是否为批量确认
        * */
        ConfirmCallback nackCallback = (deliveryTag,multiply) -> 
            System.out.println("未确认的消息:"+deliveryTag);
        ;

        // 准备消息的监听器,监听哪些消息成功,哪些消息失败
        /*
        * 参数1:监听哪些消息成功
        * 参数2:监听哪些消息失败
        * */
        channel.addConfirmListener(ackCallback,nackCallback);

        // 批量发送消息
        for (int i = 0; i < MESSAGE_COUNT; i++) 
            String message = "消息" + i;
            channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
        

        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布"+MESSAGE_COUNT+"个异步确认消息,耗时"+ (end - begin) + "ms");
    

 

如何处理异步未确认信息?

最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递

public class ComfirmMessage 

    // 批量发消息的个数
    public static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception 
        //3、异步批量确认
        // 发布1000个异步确认消息,耗时36ms
        ComfirmMessage.publicMessageAsync();

    

    public static void publicMessageAsync() throws Exception
        Channel channel = RabbitMqUtils.getChannel();
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName,false,false,false,null);

        // 开启发布确认
        channel.confirmSelect();

        /*
        * 线程安全有序的一个哈希表 适用于高并发的情况下
        * 1、轻松地将序号与消息进行关联
        * 2、轻松地批量删除,只要给到序号
        * 3、支持高并发
        * */
        ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();

        // 消息确认成功回调函数
        ConfirmCallback ackCallback = (deliveryTag,multiply) -> 
            // 删除到已经确认的消息,剩下的就是未确认的消息
            if(multiply)
                ConcurrentNavigableMap<Long, String> confiremed = outstandingConfirms.headMap(deliveryTag);
                confiremed.clear();
            else 
                outstandingConfirms.remove(deliveryTag);
            

            System.out.println("确认的消息:"+deliveryTag);
        ;

        // 消息确认失败回调函数
        /*
        * 参数1:消息的标记
        * 参数2:是否为批量确认
        * */
        ConfirmCallback nackCallback = (deliveryTag,multiply) -> 
            // 打印一下未确认的消息都有哪些
            String message = outstandingConfirms.get(deliveryTag);
            System.out.println("未确认的消息是:" + message +"未确认的消息tag:" + deliveryTag);
        ;

        // 准备消息的监听器,监听哪些消息成功,哪些消息失败
        /*
        * 参数1:监听哪些消息成功
        * 参数2:监听哪些消息失败
        * */
        channel.addConfirmListener(ackCallback,nackCallback);

        // 开始时间
        long begin = System.currentTimeMillis();

        // 批量发送消息
        for (int i = 0; i < MESSAGE_COUNT; i++) 
            String message = "消息" + i;
            channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));

            // 此处记录下所有要发送的消息的总和
            outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
        



        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布"+MESSAGE_COUNT+"个异步确认消息,耗时"+ (end - begin) + "ms");
    

6、三种发布确认速度对比

 

以上是关于RabbitMQ之持久化不公平分发 预取值发布确认模式的主要内容,如果未能解决你的问题,请参考以下文章

RabbitMQ——消息手动应答队列/消息持久化不公平分发预取值的概念理解及应用举例

RabbitMQ学习笔记(自用)

RabbitMQ消息队列笔记

RabbitMQ消息队列笔记

RabbitMQ消息队列笔记

RabbitMQ-08 不公平分发与预取值