RabbitMQ-06 消息应答

概念:

        消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。 为了保证消息在发送过程中不丢失,rabbitmq引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了。 

自动应答

        消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。

        简而言之就是只要接收到消息队列中就会移除掉此消息,所以当我们接收到消息后后面逻辑存在问题或者突然断开tcp时,会造成消息的丢失,所以不推荐使用。

 消息应答的方法  

A.Channel.basicAck(用于肯定确认)  RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了

 B.Channel.basicNack(用于否定确认)  

C.Channel.basicReject(用于否定确认) 与Channel.basicNack相比少一个参数  不处理该消息了直接拒绝,可以将其丢弃了 

在信道的应答方法中,这里拿肯定确认应答举例

 

 其中中个参数为multiple,类型为boolean

        true代表批量应答channel上未应答的消息  比如说channel上有传送tag的消息 5,6,7,8 当前tag是8 那么此时  5-8的这些还未应答的消息都会被确认收到消息应答  false同上面相比  只会应答tag=8的消息 5,6,7这三个消息依然不会被确认收到消息应答(正常为了安全考虑不采用批量应答,而是每一次只应答当前tag对应的消息,即multiple=false)

 

         如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。         

 

 例如上图,队列中有三个消息 (消息1、消息2、消息3)

C1消费消息1,C2消费消息2,C1消费没完成,由于某种原因消费失败,所以没有ACK确认消费掉

这个时候队列发现C1并没有Ack就让消息1重新入队

C2正常消费完消息2后继续消费C1没有消费成功的消息1

以此可以确保没有被C1消费成功的消息被C2消费(消息不丢失)

手动应答消息不丢失,未ack的消息重新放回队列中重新消费demo

生产者代码

public class Task2 {

    // 队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 生成队列
         *1.队列名称
         *2.队列里面的消息是否进行持久化(磁盘) 默认情况消息存储在内存中
         *3.该队列是否只提供一个消费者进行消费 是否进行消息的共享 true可以多个消费者消费
         *4.是否自动删除 最后一个消费者开连接以后  该队列是否自动删除 true自动删除
         *5.其他参数 延迟 死信等
         */
        channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
        // 从控制台输入
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            /**
             * 发送一个消息
             * 1.发送到哪个交换机
             * 2.路由的key值是哪一个 本次是队列名称
             * 3.其他参数信息
             * 4.发送消息的消息体
             * 5.getBytes("UTF-8") 中文汉字进行二进制转换时防止乱码
             */
            channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
            System.out.println("生产者发出消息"+message);

        }
    }
}

由于测试时会模拟不同的休眠时间,所以要写两个消费者,而不像之前可以开启两次同一个程序

休眠工具类SleepUtils

public class SleepUtils {
    public static void sleep(int second) {
        try {
           // 休眠直接*1000 调用sleep方法单位则为秒
            Thread.sleep(1000 * second);
        } catch (InterruptedException _ignored) {
            Thread.currentThread().interrupt();
        }
    }
}

消费者1代码

public class Worker02 {
    // 队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息,处理时间较短");
        // 消息消费的时候如何处理消息
        DeliverCallback deliverCallback = (consumerTag, delivery)->{
            String message = new String(delivery.getBody());
            // 工具类中对传入参数的sleep*1000,所以传入的参数second为秒
            SleepUtils.sleep(1);
            System.out.println("接收到的消息"+message);
            /**
             * 1.消息标记tag
             * 2.是否批量应答为应答消息(不建议开启)
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 取消接收消息回调
        CancelCallback cancelCallback = consumerTag->{
            System.out.println(consumerTag+"消息者取消消费接口回调逻辑");
        };
        // 采用手动应答
        boolean autoAck = false;
        channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}

消费者2代码

public class Worker03 {
    // 队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息,处理时间较长");
        // 消息消费的时候如何处理消息
        DeliverCallback deliverCallback = (consumerTag, delivery)->{
            String message = new String(delivery.getBody());
            // 工具类中对传入参数的sleep*1000,所以传入的参数second为秒
            // 休眠30s
            SleepUtils.sleep(30);
            System.out.println("接收到的消息"+message);
            /**
             * 1.消息标记tag
             * 2.是否批量应答为应答消息(不建议开启)
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 取消接收消息回调
        CancelCallback cancelCallback = consumerTag->{
            System.out.println(consumerTag+"消息者取消消费接口回调逻辑");
        };
        // 采用手动应答
        boolean autoAck = false;
        channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}

先进行测试,这个时候先启动消费者会报错

 报错很清晰,没有这个队列'ack_queue'在vhost中,经过查看确实没有

 所以我们需要先启用生产者代码,将队列加入进去

 ip一定要配置一个固定的,当你切换网络的时候你的ip可能会变,当ip变了的时候你建立的连接工厂所配置的ip,用户名密码等就要做相应的改变了,这里建议直接修改适配器ipv4地址为一个指定的ip。

生产者启用完毕后,启动worker02与worker03,分别发送aa,bb

Worker02在1s后控制台打印:

 Worker03在30s后控制台打印:

 

 这个时候我们在生产者控制台继续输入cc dd,但是在输入完dd间隔几秒后停掉Worker03

 正常情况下dd消息应该为C2来处理,但是由于在ack确认应答前我们关闭了程序Worker03,所以消息被重新入队,改由现存的Worker02来处理。所以消息并没有丢失,而是重新入队。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

那山川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值