06-消息应答

一、概念

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

二、自动应答

消息发送后立即被认为已经传送成功,这种模式需要在“高吞吐量和数据传输安全性方面做权衡”

  • 因为这种模式如果消息在接收到之前,消费者出现连接或者Channel关闭,那么消息就丢失了
  • 另一方面这种模式消费者那么可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死
  • 所以这种模式仅适用在消费者可以高效并以某种速度能够处理这些消息的情况下使用

Multiple

channel.basicAck(tag, multiple)
  • multiple为true:代表批量应答 channel 上未应答的消息,当channel上有tag为5、6、7多条消息,那么此时5-7中任何一条消息应答,代表5-7所有消息都应答
  • multiple为false:只会应答相应消息

自动应答存在问题

Producer
/**
 * 消息生产者
 */
public class Producer {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";


    public static void main(String[] args) throws Exception {

        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        /**
         * 创建一个队里
         *
         * 队列名称
         * 队里里面的消息是否持久化
         * 该队列是否只供一个消费者进行消费,是否进行共享,true 可以多个消费者共享
         * 是否自动删除,最后一个消费者端开连接以后,该队列是否自动删除 true 自动删除
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);


        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            //准备消息
            String msg = scanner.next();

            /**
             * 发布消息
             *
             * 发送到那个交换机
             * 路由key
             * 其他参数
             * 发送消息的消息体
             */
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
            System.out.println("发送消息完成:" + msg);
        }
    }

}
Consumer消费者耗时1s
/**
 * 接收消息
 */
public class Consumer {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        System.out.println("C等待接收消息。。。。。");

        // 接收消息回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String msg = new String(message.getBody());
            System.out.println(msg);
            // 单个应答
//            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 取消消息回调
        CancelCallback cancelCallback = (String consumerTag) -> {
            System.out.println("消息消费被中断。。。");
        };

        /**
         * 消费消息
         *
         * 消息队列
         * 消费成功之后是否要自动应答
         * 消费成功/失败回调
         */
        //标记消息手动应答
        boolean autoAck = true;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}
Consumer1消费耗时 20s
/**
 * 接收消息
 */
public class Consumer1 {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        System.out.println("C1等待接收消息。。。。。");

        // 接收消息回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String msg = new String(message.getBody());
            System.out.println(msg);
            // 单个应答
//            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 取消消息回调
        CancelCallback cancelCallback = (String consumerTag) -> {
            System.out.println("消息消费被中断。。。");
        };

        /**
         * 消费消息
         *
         * 消息队列
         * 消费成功之后是否要自动应答
         * 消费成功/失败回调
         */
        //标记消息手动应答
        boolean autoAck = true;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

消息队列
image.png

发送消息

11
发送消息完成:11
22
发送消息完成:22
33
发送消息完成:33
44
发送消息完成:44
55
发送消息完成:55
66
发送消息完成:66

Consumer消费很快

C等待接收消息。。。。。
11
33
55

Consumer1消费很慢,当重启Consumer1时消息丢失

小结:自动确认存在问题

  • 无法保证和确认消息确实被消费
  • 当消费者重启时会丢消息

三、消息自动重新入队

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

将消费者设置为“手动应答”模式,就默认会将未消费的消息重新入队

Producer


/**
 * 消息生产者
 */
public class Producer {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";


    public static void main(String[] args) throws Exception {

        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        /**
         * 创建一个队里
         *
         * 队列名称
         * 队里里面的消息是否持久化
         * 该队列是否只供一个消费者进行消费,是否进行共享,true 可以多个消费者共享
         * 是否自动删除,最后一个消费者端开连接以后,该队列是否自动删除 true 自动删除
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);


        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            //准备消息
            String msg = scanner.next();

            /**
             * 发布消息
             *
             * 发送到那个交换机
             * 路由key
             * 其他参数
             * 发送消息的消息体
             */
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
            System.out.println("发送消息完成:" + msg);
        }
    }

}

Consumer1

/**
 * 接收消息
 */
public class Consumer1 {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        System.out.println("C1等待接收消息。。。。。");

        // 接收消息回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String msg = new String(message.getBody());
            System.out.println(msg);
            // 单个应答
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 取消消息回调
        CancelCallback cancelCallback = (String consumerTag) -> {
            System.out.println("消息消费被中断。。。");
        };

        /**
         * 消费消息
         *
         * 消息队列
         * 消费成功之后是否要自动应答
         * 消费成功/失败回调
         */
        //标记消息手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

Consumer2

/**
 * 接收消息
 */
public class Consumer2 {

    // 队列名称
    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        // 创建一个Channel
        Channel channel = ConnectionUtils.getConnection().createChannel();

        System.out.println("C2等待接收消息。。。。。");

        // 接收消息回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String msg = new String(message.getBody());
            System.out.println(msg);
            // 单个应答
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 取消消息回调
        CancelCallback cancelCallback = (String consumerTag) -> {
            System.out.println("消息消费被中断。。。");
        };

        /**
         * 消费消息
         *
         * 消息队列
         * 消费成功之后是否要自动应答
         * 消费成功/失败回调
         */
        //标记消息手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

生成者生成消息

11
发送消息完成:11
22
发送消息完成:22
33
发送消息完成:33
44
发送消息完成:44
55
发送消息完成:55
66
发送消息完成:66

消费者Consumer2,由于它消费速度很慢,所以当它在消费第二个消息但是还没有完成时,停掉Consumer2,所以Consumer2真实的只消费了1个消息

11

消费者Consumer1,由于它消费速度很快,当它快速消费完它分配到的22、44、66消息后,一直处于空闲状态,当停掉Consumer2时,Consumer1立即由消费了33、55消息。

22
44
66
33
55

小结:手动应答会让为消费消息重新入队,“不会造成消息丢失”,这只是消息发送层面,手动应答不会出现消息丢失

你可能感兴趣的:(06-消息应答)