RabbitMQ深度历险

⭐RabbitMQ深度历险 - 生产者发布确认机制

首先聊聊mq(有人叫broker)服务器的确认机制。这里举出三种,分别为单个确认、批量确认、异步批量确认

  • 单个确认:

    • 单个确认就不用讲了,每一条消息都会确认一次
    • 优点:可靠,保证消息的可靠传输(不保证不会丢失)
    • 缺点:效率低(测试1000条消息时,全部确认完在1480ms左右)
  • 批量确认:

    • 批量确认就是每一次不管发多少消息,有多少消息,mq都会依次性全部确认(此时是同步的)
    • 优点:效率高(测试1000条消息时,全部确认在78ms左右)
    • 缺点:不可靠,不管剩下的消息有没有发送完,此时都会全部确认
  • 异步批量确认:

    • 当生产者发消息给mq时,采用异步方式进行发送,确认机制也是批量的,但是不会确认全部,这取决于mq的调度机制,发送消息快时,mq会批量确认许多条,发送消息慢时,mq会一条一条确认(盲猜mq的一个缓存消息容器内此时有多少条mq就会批量确认多少条)
    • 优点:效率是三者中最高的(因为发消息是异步的)
    • 缺点:不可靠,发消息过快时也会存在消息错误确认问题(ack回调函数跳着执行)

闲聊:

开篇:今天在群里有人问为什么ConfirmCallback回调函数在异步批量确认时有的消息会跳过去?

这里模拟发送1000条消息给mq,然后调用ConfirmCallback回调函数的情况(文中注释内容为测试时使用的部分代码)

// 异步发布确认
    public static void publishMessageAsync() throws Exception {
        // 获取信道
        Channel channel = RabbitMqUtils.getChannel();
        // 队列的声明
        String queueName = UUID.randomUUID().toString();
        System.out.println(queueName);
        channel.queueDeclare(queueName, true, false, false, null);

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

        // 消息确认成功回调函数
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
//            if (deliveryTag == 50) {
//                System.out.println("========50=======");
//                try {
//                    Thread.sleep(10000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }
            hashMap.put(deliveryTag, "");
            if (multiple) {
                // 2、删除掉已经确认的消息,剩下的就是未确认的消息
                // headMap()方法用于返回此元素及之前的所有元素作为一个新的集合
                ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag);
                // 清空本次所有确认的消息的集合
                confirmed.clear();
            } else {
                outstandingConfirms.remove(deliveryTag);
            }
            System.out.println("确认的消息:" + deliveryTag);
//            System.out.println("hashMap:" + hashMap.size());
        };
//        Thread.sleep(3000);
//        System.out.println("------------------" + hashMap.size());
        // 消息确认失败回调函数
        /**
         * 参数1:消息的标记
         * 参数2:是否为批量确认
         */
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            // 3、打印一下未确认的消息有哪些
            String message = outstandingConfirms.get(deliveryTag);
            System.out.println("未确认的消息:" + message);
        };

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

        // 开始时间
        long begin = System.currentTimeMillis();
        // 批量发布消息
        for (int i = 0; i < MESSAGE_COUNT; i++) {

//            Thread.sleep(1000);

            if (i > 500) {
                Thread.sleep(10000);
            }

            String message = "消息" + i;
            // 1、此处记录下所有要发送的消息(消息的总和)
            outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
            // 发布消息
            channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时:" + (end - begin) + "ms");

    }

测试结果:

在这里插入图片描述

可以看的确实ack回调函数执行时候,有的消息被跳过去了,那么这是为什么呢?

疑问:这是bug么?

答案:不是(本文开头已说明异步批量确认机制大概的原理)

当时我在这群友探讨这个问题时,以为这是个bug,结果网上查了好多资料也没结果,然后还跑去问了作者 = =

作者回复群友:

在这里插入图片描述

看到这个回复当时以为作者在敷衍人(我想用异步批量确认,为什么要我换成批量确认?不是答非所问么?);先在这里跟作者说声对不起…

因为当时没解决,所以我们又展开了激烈的探讨…

因为怀疑是发送太快,所以将生产者线程休眠了2ms,发现ack回调函数不会跳了!
在批量发消息时添加以下休眠代码:

在这里插入图片描述

在这里插入图片描述

  • 是保证了消息的可靠传输,但可以看出这样子线程之间就达不到异步发消息然后批量确认目的了

当时不知道异步确认机制也是批量的,所以又提出了猜想

猜想:会不会是队列创建时候没有初始化的问题?

实验:将声明队列的代码注释掉后,然后将queueName换成了一个已经存在的队列:

在这里插入图片描述

  • 小结:和队列有没有初始化无关

当时又想,这些队列都是存在的,那如果生产者给不存在的队列发消息会怎么样?底层会帮我们创建队列么?ack回调函数还会跳么?

实验:将这一行代码注释掉

在这里插入图片描述

结果:

在这里插入图片描述

wok,发现ack回调函数不会跳了,这是为什么呢?

猜想:会不会mq帮我们创建了一个临时的队列?

实验:在以下位置添加如下代码,当发送过半消息时,我们看一看mq有没有帮我们生成临时队列

在这里插入图片描述

结果:

在这里插入图片描述

  • 小结:当生产者发消息给不存在的队列时候,mq并不会帮我们创建一个临时的队列

问题:既然这样,那ack回调函数为什么还会执行呢?mq不是没把消息存到队列里么?

猜想:ConfirmCallback回调函数是一个假函数,当生产者发消息给mq时就自动调用了,没有等mq应答

实验:当生产者发一半时候,将mq服务器停掉,看看还会不会确认

在这里插入图片描述

  • 小结:可以看到这里代码直接报错,说连接异常(因为这里报错是连接关闭,所以不能得出结论)

问题:现在就有两种可能了,一种是ack回调函数不是假函数,一种是假函数

猜想:ack回到函数不是假函数

实验:debug

在这里插入图片描述

  • 小结:我在看到这里时候注意到了multiple这个属性,再结合作者之前所说的,终于明白了异步确认机制也是批量的,但是具体一次确认多少这要根据mq的调度机制了(erlang语言写的,我也看不懂源码 = =)

问题:现在明白了这个确认机制,但是ack回调函数还没解决,为什么不存在的队列也会触发ack回调函数?

实验:生产者发送一半消息时,消费者去消费该队列的消息

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以看出消费者直接报错,生产者还在发消息,并且ack回调函数也会执行

结合以上几个小测试,得出以下结论:
声明:以下结论都是基于猜想和测试得出的结论,不一定是正确的,仅供参考

  • 1、当生产者给mq服务器发消息时候,如果指定队列不存在,那么不会报错(消费者会报错),此时消息发送给mq,mq拿着消息去找对应队列,因为队列不存在,所以消息丢失了;
  • 2、那此时消息丢失了为什么ack回调函数还会执行?原因在于mq的确认机制,他在收到消息时候就会告诉生产者说“我收到消息了”此时生产者就会触发confirm事件调用ack回调函数,但是具体mq有没有把消息存到队列中,这个我们就不知道了;所以这个ack回调函数的作用就是确保mq收到消息,不保证可靠存储
  • 3、作者所说的basic.ack的multiple值的问题,该值默认是true,也就是说明异步确认机制是批量的;所以看到这里也不难理解,如果异步不是批量的,那么还要单个确认机制干什么;所以每种确认机制各有优点缺点,具体应用哪一种要根据实际业务场景来选择
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值