RabbitMQ干货讲解一(实战与理论并存)

一起探讨问题吧,后期会分享大量实战经验。在这里插入图片描述

概念

MQ,消息队列, 队列的特征:先进先出, 一般在实战中用于上下游传递消息。比如服务A通过消息队列向服务B发送消息,供服务B消费。 在没有MQ中间件时期,大部分都是接口互调,在被调用方返回值中存放需要的数据,或者回调函数提示success,failed;

三大特点

流量消峰:假设下单系统极限是100次/秒,当超过了100,服务器宕机,使用消息队列mq做缓冲,可以将100以后下单的访问请求先存入到mq中,等待被消费。 缺点:用户体验可能差一点,总比服务器挂掉好。 例子:高峰期下单时,转圈圈。

应用解耦:三个中一个出现异常, 调用方也必定异常。订单系统执行完之后会发送消息至mq, 如果三个有一个坏掉, 消息会监听,直至恢复正常. 类似于专业线业务挂掉了, 平台线的消息还放在mq里面等待消费。
在这里插入图片描述

异步处理:有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,
以前解法:A 提供一个 回调notify接口,B 执行完之后调用notify通知 A 服务
MQ解法::A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。

在这里插入图片描述

常用MQ的选择(二选一)

RocketMQ:底层语言是Java,阿里巴巴出品,一般应用于大型公司,毕竟承受过双11的冲击。
RabbitMQ:底层语言是Erlang,时间悠久,稳定,一般用于中小型公司。
自我感觉:没啥好大的区别,特地询问了公司的运维大佬
在这里插入图片描述

活学活用

	RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,
	当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里 

寄件人:产生数据发送消息的程序是生产者
收件人:消费数据的消费者
快递站点消息队列(队列+交换机), 交换机一方面它接收来自生产者的消息另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个要交换机类型决定

注意:
1:交换机与队列是 一对多的关系,一个快递员可以对应多个包裹
2:每个队列可以对应多个一个消费者,不过消息只能被消费一次。
3:请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。 生产者是用户服务,消费者是订单服务,消息中间件是其他服务
在这里插入图片描述

初级概念及代码,简称:有始有终

在这里插入图片描述
每一个生产者与MQ会建议一个Connection(TCP连接), 里面有多个信道, 发送消息的通道。

Exchange : message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key(图中箭头),分发消息到 queue 中去。常用的交换机类型有:direct , topic,fanout

Queue : 消息最终被送到这里等待 consumer 取走

Binding : exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

工作队列

生成者发送玩消息, 如工作队列中, 等待被消费(假设有4条消息), 不出意外的话, 会轮训被消费者消费, 且一条消息只能被一个消费者消费一次!
先看图, 再结合代码.

存在一个Name=hello的队列
在这里插入图片描述
生成者发送消息
在这里插入图片描述
两个消费者C1,C2消费消息
在这里插入图片描述

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210707224507886.png
连接工具类

/**
 * ClassName: RabbitMqUtils
 * author: bob.ly
 * Version: 1.0.0
 * DateTime: 2021/07/07-22:26:00
 * Description:链接工具类
 */
public class RabbitMqUtils {
    public static Channel getChannel() throws Exception {
        /**
         * 创建链接,得到一个信道
         */
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
    }
}

生成者

/**
 * ClassName: Productor
 * author: bob.ly
 * Version: 1.0.0
 * DateTime: 2021/07/07-22:28:00
 * Description:
 */
public class Productor {
    /**
     * 声明一个队列名称
     */
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 声明消息队列
         * param1:指定队列的名称
         * param2:是否持久化
         * param3:是否只让一个消费者消费
         * param4:是否自动删除
         * param5:其他参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /**
         * 手动从控制台当中接受信息
         */
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.next();
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("发送消息完成:" + message);

        }
    }
}

消费者:若想main开启并行模式,将main允许一次之后, C1改成C2,再允许一次main
在这里插入图片描述

public class Consumer1 {
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 推送的消息如何进行消费的接口回调
         * consumerTag: 消息叫什么名字
         * delivery: 消息内容
         */
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:" + receivedMessage);
        };
        /**
         * 取消回调:
         */
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        System.out.println("C2 消费者启动等待消费.................. ");
        /**
         * 消费者消费消息
         * 1. 消费哪个队列
         * 2. 消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3. 消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

此上为理想状态, 生成者发送消息到mq中, mq一旦向消费者传递了一条消息,便立即将该消息标记为删除, 消费者消费消息. 在非正常情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。

面试题:若消费者宕机了如何处理?

为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答机制就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。

生活场景: KFC快递员送货上门, 打开箱子, 并不是把汉堡给你了就走了, 而是要你签字, 核实完之后才离开
KFC快递员: mq
你: 消费者
签字+核实信息: 消息应答机制
箱子: 信道

消息应答机制: 自动应答 与 手动应答

api方法: channel.basicAck(用于肯定确认), 默认采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答,
在这里插入图片描述
若multiple为true, 说明mq将消息入信道(tag=1,2,3,4, 4条消息)给消费者时, 消费者消费tag=4时, 就自动把1,2,3也应答了.
若multiple为false: 说明消费哪个tag, 应答哪个tag

自动应答(少用): 这种模式仅适用在消费者可以在高并发情况下有效安全的进行,万一宕机了,消息便丢失了

手动应答(推荐): 可以批量应答并且减少网络拥堵, 手动返回ack(通知)告诉队列处理完了,队列进而删除消息。

消息自动重新入队

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

//可以copy两份, 把C1改成C2, C2睡眠10s, 等待时间较长
public class Customer1 {
    private static final String ACK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1 等待接收消息处理时间较短");
        /**
         * 消息消费的时候如何处理消息
         */
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("接收到消息:" + message);
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };
        /**
         * 采用手动应答
         */
        channel.basicConsume(ACK_QUEUE_NAME, Boolean.FALSE, deliverCallback, (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        });
    }
}

面试题:若rabbitMq挂掉了,如何保证队列和消息不丢失?

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

		/**
         * 声明消息队列
         * param1:指定队列的名称
         * param2:********是否持久化**********, 将false改为true,则开启持久化
         * param3:是否只让一个消费者消费
         * param4:是否自动删除
         * param5:其他参数
         */
        channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);

开启持久化时, 要注意必须将以前的队列删掉,重新声明。否则控制台报错,成功案例如下
在这里插入图片描述

消息持久化: 增加MessageProperties.PERSISTENT_TEXT_PLAIN

在这里插入图片描述

注意:将消息标记为持久化并不能完全保证不会丢失消息。尽管将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。

不公平分发

最开始分发消息采用的轮训分发,0,或者不设置都是轮训分发,这种策略并不是很好,比如有两个消费者在处理任务,消费者 1 处理任务的速度,而消费者 2处理速度却很,当消费者2处理任务时(未确定ack),消费者 1 一部分时间处于空闲状态,不知道干啥。
rabbitMq并不知道消费者处理的速度效率,
避免这种情况,我们可以设置参数 channel.basicQos(1),忙的先忙,不忙的继续给任务

在这里插入图片描述
开启之后如下

意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个 任务,然后 rabbitmq就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的worker 或者改变其他存储任务的策略
ps:channel.basicQos(1)每个消费者都要加上。

在这里插入图片描述

本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止一个消息,另外来自消费
者的手动确认本质上也是异步的。这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此
缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,
RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认ack。

例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。消息应答和 QOS 预取值对用户吞吐量有重大影响。

参考代码案例: https://blog.csdn.net/fan521dan/article/details/104828356?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162571270816780269846930%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162571270816780269846930&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-104828356.first_rank_v2_pc_rank_v29&utm_term=channel.basicQos&spm=1018.2226.3001.4187

面试题:生产者发送消息挂掉了(还未到磁盘上),怎么办?

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式, 所有在该信道上面发布的消
息都将会被指派一个唯一的 ID (从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会
发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,
如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出。

confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

开启发布确认的方法:单个确认发布,批量确认发布,异步确认发布
发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布
确认,都需要在 channel 上调用该方法

channel.confirmSelect(); //开启消息确认
channel.waitForConfirms(); //消息确认结果,true:成功。 false:失败

单个确认发布

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

		// 开启发布确认
		channel.confirmSelect();
		long begin = System.currentTimeMillis();
		/**
		  *循环,发一个确认一个
		  */
		for (int i = 0; i < 50; i++){ 
		String message = i + "";
		channel.basicPublish("", queueName, null, message.getBytes());
		// 服务端返回 false 或超时时间内未返回,生产者可以消息重发,true则表明已经到磁盘了
		boolean flag = channel.waitForConfirms();
		if(flag){
			System.out.println(" 消息发送成功");
		}
		}
		long end = System.currentTimeMillis();
		System.out.println(" 发布" + " 50个单独确认消息, 耗时" + (end - begin) +"ms");

批量确认发布

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

		// 开启发布确认
		channel.confirmSelect();
		long begin = System.currentTimeMillis();
		int batchSize=100;
		//分5批发送确认。
		for (int i = 0; i < 500; i++){ 
		String message = i + "";
		channel.basicPublish("", queueName, null, message.getBytes());
		// 服务端返回 false 或超时时间内未返回,生产者可以消息重发,true则表明已经到磁盘了
		if(i%batchSize==0){
			boolean flag = channel.waitForConfirms();
			if(flag){
			System.out.println(" 消息发送成功");
			}
		}
		}
		long end = System.currentTimeMillis();
		System.out.println(" 发布" + " 500个确认消息, 耗时" + (end - begin) +"ms");

异步确认发布(推荐)

性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的
在这里插入图片描述

/**
 * @author: liuyi
 * ClassName: Productor
 * Version: 1.0.0
 * DateTime: 2021/03/31-10:36:00
 * Description:异步处理
 */
public class Productor {
    public static void publishMessageAsync() throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        String queueName = UUID.randomUUID().toString();
        /**
         * 声明队列:暂不需要持久化。
         */
        channel.queueDeclare(queueName, false, false, false, null);
        /**
         * 开启发布确认,确保消息mq接受到
         */
        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, nackCallback);
        for (int i = 0; i < 100; i++) {
            String message = "消息" + i;
            /**
             * channel.getNextPublishSeqNo() 获取下一个消息的序列号
             * 通过序列号与消息体进行一个关联
             * 全部都是未确认的消息体
             */
            outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
            channel.basicPublish("", queueName, null, message.getBytes());
        }
    }
}

总结

三步必须都完成,才能确保生产者发送的消息绝对不会丢失!

在这里插入图片描述

中级概念及代码

待更新。

高级概念及代码

待更新。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RabbitMQ是一个开源的消息代理软件,它是一个在分布式系统中用于传递消息的高可用性工具。它是基于AMQP(高级消息队列协议)设计的。RabbitMQ可以在不同的应用程序之间传递消息,通过解耦应用程序的组件,提供了一种灵活、可靠的通信机制。 关于RabbitMQ的安装和配置,你可以按照以下步骤进行操作: 1. 下载RabbitMQ软件包,可以从官方网站上获取下载链接。你可以使用wget命令下载软件包。 2. 安装RabbitMQ软件包,根据你的操作系统类型,选择合适的安装方式。对于Windows系统,你可以双击安装软件包并按照安装向导进行操作。 3. 安装RabbitMQ-Plugins插件,首先进入RabbitMQ安装目录的sbin目录,然后运行命令rabbitmq-plugins enable rabbitmq_management。这将启用RabbitMQ的管理插件。 4. 检查RabbitMQ的状态,你可以运行命令rabbitmqctl status来确认RabbitMQ是否成功安装和启动。 5. 启动RabbitMQ服务器,你可以双击运行rabbitmq-server.bat文件来启动RabbitMQ服务器。一旦成功启动,你将能够在登录页面中看到RabbitMQ。 此外,你还可以使用以下命令为RabbitMQ文件设置正确的所有权: chown -R rabbitmq:rabbitmq /var/lib/rabbitmq/ 这将把RabbitMQ文件的所有权提供给RabbitMQ用户,确保正确的权限设置。 总之,RabbitMQ是一个功能强大的消息代理软件,它提供了可靠和灵活的消息传递机制。通过遵循适当的安装和配置步骤,你可以轻松地开始使用RabbitMQ来实现应用程序之间的消息传递。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [RabbitMQ详解,用心看完这一篇就够了【重点】](https://blog.csdn.net/weixin_42039228/article/details/123493937)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值