RabbitMQ入门及简单Case

 1. 为什么很多公司都选择RabbitMQ?
 
 2. RabbitMQ的高性能是如何实现的?
 
 3. AMQP高级协议&核心概念
 
 4. RabbitMQ整体架构是怎样的?
 
 5. RabbitMQ中的消息是如何流转?
 
 6. 安装&使用
 
 7. 消息的生产者&消费者
 
 8. RabbitMQ独有的Exchange交换机
 
 9. 队列(Queue)、绑定(Binding)、虚拟主机(Virtual Host)、消息(Message)

一、RabbitMQ是什么

主要是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,它主要是使用Erlang语言进行编写的,并且还基于AMQP协议。

优点:
与SpringAMQP完美结合,拥有丰富的API

集群模式相当丰富,提供表达式配置,HA模式,镜像队列模型。

二、AMQP协议

Advanced Message Queuing Protocol高级消息协议。
在这里插入图片描述

三、RabbitMQ架构图

在这里插入图片描述

四、RabbitMQ消息流转

在这里插入图片描述

五、下载和安装

5.1 下载

5.1.1 下载Erlang的rpm包

RabbitMq是Erlang语言编写的,所以Erlang环境必须要有,需要注意的是:Erlang环境一定要与RabbitMq版本匹配

Erlang下载地址:(根据自身需求及版本关系,下载对应的rpm包)

多版本Erlang下载:

5.1.2 下载socat的rpm包

rabbitmq安装依赖于socat,所以需要下载socat。

socat下载地址:

5.1.3下载RabbitMQ的rpm包

RabbitMQ下载地址:(根据自身需求及匹配关系,下载对应rpm包)

5.2 安装

5.2.1 安装Erlang、Socat、RabbitMQ

①rpm -ivh erlang-21.3.8.9-1.el7.x86_64.rpm
②rpm -ivh socat-1.7.3.2-1.el6.lux.x86_64.rpm
在安装rabbitmq之前需要先安装socat,否则,报错。
在这里插入图片描述可以采用yum安装方式:yum install socat,我们这里采用rpm安装方式
③rpm -ivh rabbitmq-server-3.8.1-1.el7.noarch.rpm
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.1/sbin

5.2.2 启用管理插件

[jg@HadoopNode01 opt]$ sudo rabbitmq-plugins enable rabbitmq_management

在这里插入图片描述

5.2.3 启动RabbitMQ

 systemctl start rabbitmq-server.service
 systemctl status rabbitmq-server.service
 systemctl restart rabbitmq-server.service
 systemctl stop rabbitmq-server.service 

在这里插入图片描述

5.2.4 查看进程

 ps -ef | grep rabbitmq

在这里插入图片描述

5.2.5 测试

  • 关闭防火墙:systemctl stop firewalld

  • 在Web浏览器输入地址:http://虚拟机ip:15672

  • 输入默认账号密码:guest:guest(guest用户默认不允许远程连接)

  • 增加自定义账号

  • 添加管理员账号:rabbitmqctl add_user admin admin

  • 分配账号角色:
    rabbitmqctl set_user_tags admin administrator

  • 修改密码:rabbitmqctl change_password admin 123456

  • 查看用户列表:rabbitmqctl list_users
    在这里插入图片描述

  • 管理界面标签页介绍

    • overview:概览
    • connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以直接查看连接情况
    • channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
    • Exchanges:交换机,用来实现消息的路由
    • Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
    • 端口:
      • 5672:rabbitMq的编程语言客户端连接端口
      • 15672:rabbitMq管理界面端口
      • 25672:rabbitMq集群的端口

5.2.6 卸载

  • rpm -qa | grep rabbitmq
  • rpm -e rabbitmq-server

六、五种消息模型

RabbitMq提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不需要太多关注,那么也就剩下五种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

在这里插入图片描述

6.1 准备工作

6.1.1 创建project或者module

6.1.2 导入依赖

    <parent>                                            
        <groupId>org.springframework.boot</groupId>         
        <artifactId>spring-boot-starter-parent</artifactId>    
        <version>2.2.2.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>    
        <dependency>                    
            <groupId>org.apache.commons</groupId>       
            <artifactId>commons-lang3</artifactId>        
            <version>3.3.2</version>    
        </dependency>    
        <dependency>       
            <groupId>org.springframework.boot</groupId>        
            <artifactId>spring-boot-starter-amqp</artifactId>    
        </dependency>    
        <dependency>        
            <groupId>org.springframework.boot</groupId>        
            <artifactId>spring-boot-starter-test</artifactId>    
        </dependency>
    </dependencies>

6.1.3 建立连接工具类

    public class ConnectionUtil {   
        public static Connection connection = null;   
        public static Connection getConnection() throws Exception {       
            //定义连接工厂       
            ConnectionFactory factory = new ConnectionFactory();       
            //设置服务地址        
            factory.setHost("192.168.170.132");       
            //设置端口号        
            factory.setPort(5672);       
            //设置账号信息,用户名,密码,vhost        
            factory.setVirtualHost("/jg");        
            factory.setUsername("jg");        
            factory.setPassword("123456");       
            //通过工厂获取连接        
            connection = factory.newConnection();        
            return connection;    
         }    
         public static void closeConnection() throws Exception {       
            if (connection != null) {            
                connection.close();       
            }   
         }   
         //测试连接    
         public static void main(String[] args) throws Exception {       
            Connection con = ConnectionUtil.getConnection();       
            System.out.println(con);        
            con.close();    
            }
         }

6.2 基本消息类型

RabbitMQ是一个消息代理:它接收和转发消息。你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终把邮件发送给你的收件人。

在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。

RabbitMQ与邮局的主要区别就是它不处理信息,而是接收,存储和转发数据消息的二进制数据块。

在这里插入图片描述

  • P(producer/ publisher):生产者,一个发送消息的用户应用程序。
  • C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
  • 队列(红色区域):rabbitmq内部类似于邮箱的一个概念。
  • 虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。
  • 队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。
  • 许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

总之:

  • 生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。
  • 我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。
  • 我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。
  • 我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。
  • 发布者将连接到RabbitMQ,发送一条消息,然后退出。

6.2.1 生产者发送消息

    public class Send {
        //定义要发送的队列
        private final static String QUEUE_NAME = "simple_queue";
    
        public static void main(String[] args) throws Exception{
            //获取连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中创建通道
            Channel channel = connection.createChannel();
            //声明/创建队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //消息内容
            String message = "Hello World";
            //向指定的队列中发送消息
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
            //管理通道和连接
            channel.close();
            ConnectionUtil.closeConnection();
        }
    }

6.2.2 消费者接收消息

    public class Receive {
        //声明要接收消息的队列
        private final static String QUEUE_NAME = "simple_queue";
    
        public static void main(String[] args) throws Exception{
            //获取连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中创建通道
            Channel channel = connection.createChannel();
            //声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
    
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String (delivery.getBody(),"UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            };
            channel.basicConsume(QUEUE_NAME,true,deliverCallback,consumerTag -> {});
        }
    }

6.2.3 消息确认机制(ACK)

  • 通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。
  • 那么问题来了:RabbitMQ怎么知道消息被接收了呢?
  • 如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!
  • 因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。
  • 不过这种回执ACK分两种情况:
    • 自动ACK:消息一旦被接收,消费者自动发送ACK
    • 手动ACK:消息接收后,不会发送ACK,需要手动调用。
  • 具体怎么选择要看消息的重要性
    • 如果消息不太重要,丢失也没有影响,那么自动ACk会比较方便
    • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
  • 我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码:
    public class Receive2 {
        //声明要接收消息的队列
        private final static String QUEUE_NAME = "simple_queue";
    
        public static void main(String[] args) throws Exception{
            //获取连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中创建通道
            Channel channel = connection.createChannel();
            //声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
    
            //定义队列的消费者
            DefaultConsumer consumer = new DefaultConsumer(channel){
                /**
                 *   获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
                 *   即使已经完成了队列中信息的消费,程序还是不会停止,一直在监听队列中是否有新的消息。
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    //body即消息体
                    String message = new String(body);
                    System.out.println(" [x] received : " + message + "!");
                    //手动进行ACK multiple:是否同时确认多个消息
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            };
            /**
             * @param autoAck 是否自动进行消息确认
             */
            channel.basicConsume(QUEUE_NAME,false,consumer);
        }
    }

6.3 work模型

工作队列或者竞争消费者模式

在这里插入图片描述

在使用基本消息模型的时候,是从一个命名队列中发送和接收消息的。在这里我们将创建一个工作队列,在多个工作者之间分配耗时任务。

工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队。在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。

这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。

接下来我们来模拟这个流程:

  • P:生产者:任务的发布者
  • C1:消费者1:领取任务并且完成任务,假设完成速度较快
  • C2:消费者2:领取任务并且完成任务,假设完成速度慢

面试题:避免消息堆积?

  1. 采用workqueue,多个消费者监听同一队列
  2. 接收到消息后,通过线程池异步消费

6.3.1 生产者

    public class Send {
        //定义队列
        private final static String QUEUE_NAME = "test_work_queue";
    
        public static void main(String[] args) throws Exception {
            //建立连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中建立通道
            Channel channel = connection.createChannel();
            //声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //循环发布消息
            for (int i = 0; i < 50; i++) {
                //定义消息内容
                String message = "task ......"+i;
                //发布消息
                channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
                System.out.println(" [x] Sent '" + message + "'");
            }
            //关闭通道和连接
            channel.close();
            ConnectionUtil.closeConnection();
        }
    }

6.3.2 消费者1

    public class Receive {
        //TODO:定义队列
        private final static String QUEUE_NAME = "test_work_queue";
    
        public static void main(String[] args) throws Exception {
    
            //TODO:建立连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //TODO:设置每个消费者同时只能消费一条消息
    //        channel.basicQos(1);
            //TODO:定义消费者
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者1] received :" + message + "!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //手动ACK
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            };
            //监听队列
            channel.basicConsume(QUEUE_NAME,false,consumer);
        }
    }

6.3.3 消费者2

    public class Receive2 {
        //TODO:定义队列
        private final static String QUEUE_NAME = "test_work_queue";
    
        public static void main(String[] args) throws Exception {
    
            //TODO:建立连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //TODO:设置每个消费者同时只能消费一条消息
    //        channel.basicQos(1);
            //TODO:定义消费者
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者2] received :" + message + "!");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //手动ACK
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            };
            //监听队列
            channel.basicConsume(QUEUE_NAME,false,consumer);
        }
    }

消费者1消费消息

在这里插入图片描述

消费者2消费消息

在这里插入图片描述

可以发现,两个消费者各自消费了25条消息,而且各不相同,这就实现了任务分发。

6.3.4 能者多劳

  • 刚才的实现有问题吗?
    消费者1比消费者2的效率要低,一次任务的耗时较长,然而两人最终消费的消息数量是一样的,消费者2大量时间处于空闲状态,消费者1一直忙碌。
  • 现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多;
  • 那么要怎么实现呢?
    我们可以使用basicQos方法和prefetchCount=1设置,这告诉RabbitMQ一次不要向消费者发送多于一条消息,或者换句话说,不要向消费者发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个消费者。

6.4 订阅模型分类

在之前的模式中,我们创建了一个工作队列。工作队列背后的假设是:每个任务只被传递给一个消费者。

在这一部分,我们将做一些完全不同的事情,我们将会传递一个消息给多个消费者。我们称这种模式为“发布-订阅”。

订阅模型示意图:

在这里插入图片描述

解读:

  1. 1个生产者,多个消费者
  2. 每个消费者都有自己的一个队列
  3. 生产者没有将消息直接发送到队列,而是发送到了交换机
  4. 每个队列都要绑定到交换机
  5. 生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的。
  6. Exchange
    交换机一方面接收生产者发送的消息,另一方面知道如何处理消息,例如递交给某个特别队列、递交给所有队列,或是将消息丢弃。到底如何操作,取决于Exchange的类型。
    Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有任何符合路由规则的队列,那么消息会丢失。

Exchange类型有以下几种:

  • Fanout:广播,将消息交给所有绑定到交换机的队列
  • Direct:定向,把消息交给符合指定routing key的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式)的队列

6.5 订阅模型-Fanout

6.5.1 生产者

    public class Send {
        //TODO:定义exchange的名称
        private final static String EXCHANGE_NAME = "fanout_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //TODO:建立连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明exchange,并指定类型为fanout
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
            //TODO:消息内容
            String message = "Hello Fanout";
            //TODO:发布消息到Exchange
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
            System.out.println(" [生产者] Sent '" + message + "'");
            //关闭通道释放连接
            channel.close();
            ConnectionUtil.closeConnection();
        }
    }

6.5.2 消费者1

    public class Receive1 {
        private final static String QUEUE_NAME = "fanout_exchange_queue_1";
        private final static String EXCHANGE_NAME = "fanout_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //获取连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中获取通道
            final Channel channel = connection.createChannel();
            //声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //绑定队列到交换机
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
            //定义队列消费者
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者1] received : " + message + "!");
                }
            };
            //监听队列
            channel.basicConsume(QUEUE_NAME,true,consumer);
        }
    
    }

6.5.3 消费者2

    public class Receive2 {
        private final static String QUEUE_NAME = "fanout_exchange_queue_2";
        private final static String EXCHANGE_NAME = "fanout_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //获取连接
            Connection connection = ConnectionUtil.getConnection();
            //从连接中获取通道
            final Channel channel = connection.createChannel();
            //声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //绑定队列到交换机
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
            //定义队列消费者
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者2] received : " + message + "!");
                }
            };
            //监听队列
            channel.basicConsume(QUEUE_NAME,true,consumer);
        }
    }

6.5.4 测试

我们应该先启动生产者,否则,先启动消费者时,由于要绑定交换机,此时,交换机不存在所以会报错。这就跟我们订阅微信公众号一样,如果公众号不存在的话,显然是不能订阅的。

6.6 订阅模型-Direct

  • 有选择的接收消息。
  • 在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
  • 在路由模式中,我们将添加一个功能-我们将只能订阅一部分消息。
    例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
  • 但是在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
  • 在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

在这里插入图片描述

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给routing key完全匹配的队列
  • C1:消费者,其所在队列指定了需要routing key为error的消息
  • C2:消费者,其所在队列指定了需要routing key为info、error、waring的消息

6.6.1 生产者

    public class Send {
        private final static String EXCHANGE_NAME = "direct_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //TODO:获取连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明通道,并指定通道的类型
            channel.exchangeDeclare(EXCHANGE_NAME,"direct");
            //TODO:消息内容
            String message = "商品新增了,id=1001";
            //TODO:发送消息,并且指定routing key为insert,代表新增商品
            channel.basicPublish(EXCHANGE_NAME,"insert",null,message.getBytes());
            System.out.println(" [商品服务:] Sent '" + message + "'");
            //关闭通道,释放连接
            channel.close();
            ConnectionUtil.closeConnection();
        }
    }

6.6.2 消费者1

    public class Receive1 {
        private final static String QUEUE_NAME = "direct_exchange_queue_1";
        private final static String EXCHANGE_NAME = "direct_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //TODO:获取连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //TODO:绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
            //TODO:定义队列的消费者
            DefaultConsumer consumer = new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    //消息体
                    String message = new String(body);
                    System.out.println(" [消费者1] received : " + message + "!");
                }
            };
            //监听队列,自动ACK
            channel.basicConsume(QUEUE_NAME,true,consumer);
        }
    }

6.6.3 消费者2

我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。

    public class Receive2 {
        private final static String QUEUE_NAME = "direct_exchange_queue_2";
        private final static String EXCHANGE_NAME = "direct_exchange_test";
    
        public static void main(String[] args) throws Exception{
            //TODO:获取连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:根据连接获取通道
            final Channel channel = connection.createChannel();
            //TODO:声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //TODO:绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"insert");
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
            //TODO:定义队列的消费者
            DefaultConsumer consumer = new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者2] received : " + message + "!");
                }
            };
            //TODO:监听队列,自动ACK
            channel.basicConsume(QUEUE_NAME,true,consumer);
        }
    }

6.7 订阅模型-Topic

  • Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing Key的时候使用通配符

  • RoutingKey一般都是由一个或者多个单词组成,多个单词之间以"."分割,例如:item.insert

  • 通配符规则:

    • ‘#’:匹配0或多个词(含零个)
    • ‘*’:匹配不多不少恰好一个词(不含零个)
  • 举例:

    • ‘audit#’:能够匹配’audit.irs.corporate’或者’auditirs’
    • ‘audit*’:只能匹配’audit.irs’
      在这里插入图片描述
  • 在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:".."。

  • 我们创建三个绑定:Q1绑定了绑定键"* .orange.* “,Q2绑定了”* .*rabbit"和"lazy.#"。

  • Q1匹配所有的橙色动物。

  • Q2匹配关于兔子以及懒惰动物的消息。

  • 练习,生产者发送如下消息,会进入那个队列:

    • quick.orange.rabbit
    • lazy.orange.elephant
    • quick.orange.fox
    • lazy.pink.rabbit
    • quick.brown.fox
    • quick.orange.male.rabbit
    • orange

6.7.1 生产者

使用topic类型的Exchange,发送消息的routing key有三种:item.insert、item.update、item.delete。

    public class Send {
        private final static String EXCHANGE_NAME = "topic_exchange_test";
    
        public static void main(String[] args) throws Exception {
            //TODO:获取连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接中获取通道
            Channel channel = connection.createChannel();
            //TODO:声明exchange并指定类型为topic
            channel.exchangeDeclare(EXCHANGE_NAME,"topic");
            //TODO:指定要发送消息的内容
            String message = "新增商品:id=1001";
            //TODO:发送消息
            channel.basicPublish(EXCHANGE_NAME,"item.insert",null,message.getBytes());
            System.out.println(" [商品服务:] Sent '" + message + "'");
            //TODO:关闭通道,释放连接
            channel.close();
            ConnectionUtil.closeConnection();
        }
    }

6.7.2 消费者1

    public class Receive1 {
        private final static String QUEUE_NAME = "topic_exchange_queue_1";
        private final static String EXCHANGE_NAME = "topic_exchange_test";
    
        public static void main(String[] args) throws Exception {
            //TODO:获取连接
            Connection connection = ConnectionUtil.getConnection();
            //TODO:从连接种获取通道
            Channel channel = connection.createChannel();
            //TODO:声明队列
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            //TODO:绑定队列到交换机上,同时指定需要订阅的routing key
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.delete");
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.update");
            //TODO:定义队列的消费者
            DefaultConsumer consumer = new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者1] received : " + message + "!");
                }
            };
            //TODO:监听队列,自动ACK
            channel.basicConsume(QUEUE_NAME,true,consumer);
    
        }
    }

6.7.3 消费者2

    public class Receive2 {
        private final static String QUEUE_NAME = "topic_exchange_queue_2";
        private final static String EXCHANGE_NAME = "topic_exchange_test";
    
        public static void main(String[] args) throws Exception{
            Connection connection = ConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.*");
            DefaultConsumer consumer = new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body);
                    System.out.println(" [消费者2] received : " + message + "!");
                }
            };
            channel.basicConsume(QUEUE_NAME,true,consumer);
        }
    }

6.8 持久化

  • 如何避免消息丢失?
    • 消费者的ACK机制。可以防止消费者丢失消息
    • 但是,如果在消费者消费之前,MQ就宕机了,消息就没了
  • 如何将消息进行持久化?
    • 要将消息持久化,前提是:队列、Exchange都持久化

6.8.1 交换机持久化和消息持久化

在这里插入图片描述

6.8.2 队列持久化

在这里插入图片描述

七、Spring AMQP

7.1 简介

Spring有很多不同的项目,其中就有对AMQP的支持:

在这里插入图片描述

SpringAMQP的页面

在这里插入图片描述

Spring AMQP是对AMQP协议的抽象实现,而spring-rabbit是对协议的具体实现,也就是目前的唯一实现。底层使用的就是RabbitMQ。

7.2 依赖和配置

添加AMQP的启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>

在application.yml中添加RabbitMQ地址

spring:
  rabbitmq:
    host: 192.168.170.132
    username: jg
    password: 123456
    virtual-host: /jg
    port: 5672

7.3 监听者

在Spring AMOP中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。

    @Component
    public class Listener {
    
        @RabbitListener(bindings = @QueueBinding(value = @Queue(value = "spring.test.queue", durable = "true"),
                exchange = @Exchange(value = "spring.test.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
                key = {"#.#"}))
        public void listen(String msg) {
            System.out.println("接收到消息:" + msg);
        }
    }
  • @Componet:类上的注解,注册到Spring容器

  • @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:

    • bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:
    • value:这个消费者关联的队列。值是@Queue,代表一个队列
    • exchange:队列所绑定的交换机,值是@Exchange类型
    • key:队列和交换机绑定的RoutingKey

类似listen这样的方法在一个类中可以写多个,就代表多个消费者。

7.4 AmqpTemplate

Spring最擅长的事情就是封装,把他人的框架进行封装和整合。

Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息,其发送方法:

7.5 测试代码

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = SpringAmqpDemoApplication.class)
    class SpringAmqpDemoApplicationTests {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        @Test
        void testSend() throws Exception{
            String message = "hello,spring amqp";
            amqpTemplate.convertAndSend("spring.test.exchange","a.b",message);
            //等待10秒后结束
            Thread.sleep(10000);
        }
    }

结果
在这里插入图片描述

注:本文是根据尚硅谷RabbitMQ视频教程记得笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值