RabbitMQ

1. 消息队列(MQ)

1.1 消息队列的应用场景

现在有三个独立的微服务:

  • 商品微服务:原始数据保存在 MySQL 中,从 MySQL 中增删改查商品数据。

  • 搜索微服务:原始数据保存在 ES 的索引库中,从 ES 中查询商品数据。

  • 商品详情微服务:做了页面静态化,静态页面的商品数据不会随着数据库发生变化。

假如我在商品微服务中,修改了商品的数据,也就是 MySQL 中数据发生了改变。但搜索微服务查询到的数据还是原来的,商品详情微服务的生成的静态页面也没有发生改变,这样显然不对。我们需要实现数据的同步,让搜索微服务和商品详情微服务的商品也发生修改

下面有两种解决方案:

  • 方案 1:每当商品微服务进行增删改操作,同时要修改索引库数据及静态页面
  • 方案 2:商品详情微服务和搜索微服务对外提供操作接口,在商品微服务进行增删改操作后,调用接口

上面两种方式都有一个问题:就是代码耦合。商品微服务中需要调用搜索微服务和商品详情微服务,违背了微服务的独立原则。

这时,就可以使用消息队列技术来解决这个问题

  1. 在商品微服务修改商品后,就发出一条消息给消息队列,也不关心谁接受消息。
  2. 搜索微服务从消息队列接收消息,去修改索引库。
  3. 商品详情微服务从消息队列接收消息,去修改静态页面。
  4. 如果以后有其它微服务也依赖商品微服务的数据,同样监听消息即可,商品微服务无需任何代码修改。

在这里插入图片描述

1.2 消息队列的定义

消息队列,MQ(Message Queue),是基础数据结构中 “先进先出” 的一种数据机构。指把要传输的消息放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。

消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦

1.3 AMQP 和 JMS

MQ 是消息通信的模型,但并不是具体实现。

现在实现 MQ 的有两种主流方式:

  • AMQP:

    在这里插入图片描述

  • JMS:

    在这里插入图片描述

两者间的区别和联系:

  • JMS 是定义了统一的接口,来对消息操作进行统一;AMQP 是通过规定协议来统一数据交互的格式。
  • JMS 限定了必须使用 Java 语言;AMQP 只是协议,不规定实现方式,因此是跨语言的。
  • JMS 规定了两种消息模型;而 AMQP 的消息模型更加丰富。

1.4 常见 MQ 产品

  • ActiveMQ:基于 JMS
  • RabbitMQ:基于 AMQP 协议,Erlang 语言开发,稳定性好
  • RocketMQ:基于 JMS,阿里巴巴产品,目前交由 Apache 基金会
  • Kafka:分布式消息系统,高吞吐量

2. RabbitMQ

2.1 RabbitMQ 简介

RabbitMQ 是基于 AMQP 的一款消息管理系统。

2.2 Windows 下安装 RabbitMQ

2.2.1 下载安装包

  1. 进入 RabbitMQ 官网

    https://www.rabbitmq.com/download.html
    
  2. 选择 Window 版

    在这里插入图片描述

  3. 然后下载 RabbitMQ 安装包

    在这里插入图片描述

  4. 由于 RabbitMQ 的运行需要 Erlang,所以我们查看一下 RabbitMQ 支持的 Erlang 版本

    在这里插入图片描述

  5. 进入 Erlang 官网,选择合适的 Erlang 下载

    https://www.erlang.org/downloads
    

    在这里插入图片描述

  6. 这是我下载好的安装包

    在这里插入图片描述

2.2.2 安装 Erlang

  1. 双击 otp_win64_20.0.exe 安装

  2. 默认 next 就行,无脑安装

  3. 安装完成后,创建一个名为 ERLANG_HOME 的环境变量,值为 Erlang 的安装目录

    在这里插入图片描述

  4. 修改系统环境变量 Path,在 PATH 变量中添加

    %ERLANG_HOME%\bin
    

    在这里插入图片描述

  5. 打开命令行,输入 erl,出现 Erlang 的版本信息,则安装成功

    在这里插入图片描述

2.2.3 安装 RabbitMQ

  1. 双击 rabbitmq-server-3.7.9.exe 安装

  2. 默认下一步就行,无脑安装

  3. 使用 cmd 进入到安装目录的 sbin 目录下,输入命令

    rabbitmq-plugins.bat enable rabbitmq_management
    

    在这里插入图片描述

  4. 打开浏览器访问,用户名,密码为 guest

    http://localhost:15672
    

    在这里插入图片描述

  5. 成功登录,安装成功

    在这里插入图片描述

2.3 RabbitMQ 管理界面

2.3.1 主页总览

在这里插入图片描述

  • connections:无论生产者还是消费者,都需要与 RabbitMQ 建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况

  • channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。

  • Exchanges:交换机,用来实现消息的路由

  • Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。

在这里插入图片描述

可以看到 RabbitMQ 监听三个端口:

  • 5672:RabbitMq 的编程语言客户端连接端口

  • 15672:RabbitMq 管理界面端口

  • 25672:RabbitMq 集群的端口

2.3.2 添加用户

如果不想使用 guest 用户,我们也可以自己创建一个用户。

  1. 点击 Admin,再点击 Add a user

    在这里插入图片描述

  2. 填写信息后,点击 Add user

    在这里插入图片描述

2.3.3 创建虚拟主机

  1. 点击 Virtual Hosts,再点击 Add a new virtual host

    在这里插入图片描述

  2. 填写信息后,点击 Add virtual host

    在这里插入图片描述

2.3.4 设置权限

  1. 点击 /leyou 虚拟主机

    在这里插入图片描述

  2. 添加 leyou 用户,点击 Set permission

    在这里插入图片描述

  3. 添加权限成功

    在这里插入图片描述

3. 五种消息模型

RabbitMQ 提供了 6 种消息模型,但是第 6 种其实是 RPC,并不是 MQ,那么还剩 5 种需要学习。

在这里插入图片描述

3.1 创建工程

  1. 创建一个 Maven 工程

  2. 导入依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>cn.itcast.rabbitmq</groupId>
    	<artifactId>itcast-rabbitmq</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.0.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>
    </project>
    
  3. 创建一个连接 RabbitMQ 的工具类

    public class ConnectionUtil {
        /**
         * 建立与RabbitMQ的连接
         * @return
         * @throws Exception
         */
        public static Connection getConnection() throws Exception {
            //定义连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            //设置服务地址
            factory.setHost("127.0.0.1");
            //端口
            factory.setPort(5672);
            //设置账号信息,用户名、密码、vhost
            factory.setVirtualHost("/leyou");
            factory.setUsername("leyou");
            factory.setPassword("leyou");
            // 通过工程获取连接
            Connection connection = factory.newConnection();
            return connection;
        }
    }
    
    

3.2 简单模型

在这里插入图片描述

可以看到上面有三个角色:

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

简单模型:生产者发送消息到队列,消费者监听队列,如果队列有消息就会被消费掉

注意:生产者,消费者和队列不必位于同一主机上。实际上,在大多数应用程序中它们不是。一个应用程序既可以是生产者,也可以是消费者

下面我们就用 Java 客户端实现:发送单个消息的生产者,以及接收消息并将其打印出来的消费者。

3.2.1 生产者发送消息

  1. 创建 Send 类

    public class Send {
    
        private final static String QUEUE_NAME = "simple_queue";
    
        public static void main(String[] argv) throws Exception {
            // 获取到连接以及mq通道
            Connection connection = ConnectionUtil.getConnection();
            // 从连接中创建通道,这是完成大部分API的地方。
            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();
            connection.close();
        }
    }
    
  2. 运行 Send 类,发送一个消息

    在这里插入图片描述

3.2.2 管理工具中查看消息

  1. 进入队列页面,可以看到新建了一个队列:simple_queue

    在这里插入图片描述

  2. 点击队列名称,进入详情页,可以查看消息

    在这里插入图片描述

3.2.3 消费者获取消息

  1. 创建 Recv 类

    public class Recv {
        private final static String QUEUE_NAME = "simple_queue";
    
        public static void main(String[] argv) throws Exception {
            // 获取到连接
            Connection connection = ConnectionUtil.getConnection();
            // 创建通道
            Channel channel = connection.createChannel();
            // 声明队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            // 定义队列的消费者
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                        byte[] body) throws IOException {
                    // body 即消息体
                    String msg = new String(body);
                    System.out.println(" [x] received : " + msg + "!");
                }
            };
            // 监听队列,第二个参数:是否自动进行消息确认。
            channel.basicConsume(QUEUE_NAME, true, consumer);
        }
    }
    
  2. 运行 Recv 类,消费者获取消息。但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印

    在这里插入图片描述

  3. 进入队列页面,可以看到队列中的消息没了

    在这里插入图片描述

3.3 消息确认机制(ACK)

3.3.1 ACK 的概念

通过上面的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ 怎么知道消息被接收了呢

RabbitMQ 有一个 ACK 机制。当消费者获取消息后,会向 RabbitMQ 发送回执 ACK,告知消息已经被接收。

ACK 分两种:

  • 自动 ACK:消息一旦被接收,消费者自动发送 ACK。
  • 手动 ACK:消息接收后,不会发送 ACK,需要手动调用。

3.3.2 自动 ACK 存在的问题

  1. 我们上面的案例就是使用的自动 ACK,第二个参数为 true,则会自动进行ACK。

    // 监听队列,第二个参数:是否自动进行消息确认。
    channel.basicConsume(QUEUE_NAME, true, consumer);
    
  2. 修改 Recv 类,添加一个异常

    在这里插入图片描述

  3. 运行 Send 类,发送一个消息,可以看到队列中已经存在一个消息了

    在这里插入图片描述

  4. 运行 Recv 类,出现异常了

    在这里插入图片描述

  5. 可以看到,队列中的消息还是被消费掉了

    在这里插入图片描述

分析:可想而知,如果这个消息很重要,比如张三向李四转账 500 元,张三账户减少了 500,消息在李四账户增加 500 时出现了异常,RabbitMQ 还是会把消息从队列中删除,那这个转账操作肯定失败的。

3.3.3 演示手动 ACK

  1. 把 Recv 改为手动 ACK,第二个参数设置为 flase。

    // 监听队列,第二个参数:是否自动进行消息确认。
    channel.basicConsume(QUEUE_NAME, false, consumer);
    
  2. 并添加消息确认

    在这里插入图片描述

  3. 运行 Send 类,发送一个消息,可以看到队列中已经存在一个消息了

    在这里插入图片描述

  4. 运行 Recv 类,出现异常了

    在这里插入图片描述

  5. 可以看到消息没有被消费掉

    在这里插入图片描述

    原因:这是因为我们设置了手动 ACK,但是代码中并没有执行到消息确认。所以消息并未被真正消费掉,会先从 Ready 状态变为 Unasked 状态,然后一段时间再次变为 Ready。

  6. 那我们取消异常,再次执行,消息成功被消费了

    在这里插入图片描述

3.4 工作队列模型

在这里插入图片描述

工作队列模型:生产者发送消息到队列,消费者可以有多个,并且同时监听一个队列,它们共同争抢当前的消息队列消息,谁先拿到谁消费,但是一个消息只能被一个消费者消费

下面我们就用 Java 客户端模拟这个流程:

  • 生产者:任务的发布者

  • 消费者 1:领取任务并完成任务,假设完成速度较快

  • 消费者 2:领取任务并完成任务,假设完成速度较慢

怎么实现消费者 1 和消费者 2 共同争抢当前的消息队列消息呢

我们可以在两个消费者中,都设置 RabbitMQ 一次不要向消费者发送多于一条消息:

// 设置每个消费者同时只能处理一条消息
channel.basicQos(1);

3.4.1 生产者

生产者循环发送 50 条消息:

public class Send {
    private final static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] argv) 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 + "'");

            Thread.sleep(i * 2);
        }
        // 关闭通道和连接
        channel.close();
        connection.close();
    }
}

3.4.2 消费者 1

消费者 1 完成任务速度较慢:

public class Recv {
    private final static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        final Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 设置每个消费者同时只能处理一条消息
        channel.basicQos(1);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
                try {
                    // 模拟完成任务的耗时:1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                // 手动ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

3.4.3 消费者 2

消费者 2 完成任务速度较快:

public class Recv2 {
    private final static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        final Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 设置每个消费者同时只能处理一条消息
        channel.basicQos(1);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
                // 手动ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

3.4.4 测试

  1. 运行两个消费者 Recv、Recv2

  2. 运行生产者 Send

  3. 查看 Recv 运行结果

    在这里插入图片描述

  4. 查看 Recv2 运行结果

    在这里插入图片描述

分析:可以看到速度较快的消费者 2 消费了更多的消息。

3.5 发布/订阅模型

前面的模型中,我们的一个消息只会被一个消费者消费。但有些情况,我们的一个消息需要被多个消费者消费,“发布/订阅模型” 就可以满足这一需求

在这里插入图片描述

可以看到上面有四个角色:

  • P:生产者。
  • X:交换机。生产者没有将消息直接发送到队列,而是发送到了交换机,交换机再将消息递交给所有队列。
  • 队列:每个队列都要绑定到交换机。
  • 消费者:每一个消费者都有自己的一个队列。

注意:

  • 在发布/订阅模型中,交换机的类型为 Fanout(广播),它会将消息交给所有绑定到交换机的队列
  • 交换机只负责转发消息,不具备存储消息的能力。因此如果没有任何队列与交换机绑定,或者没有符合路由规则的队列,那么消息会丢失。

发布/订阅模型:生产者将消息发送给交换机,交换机把消息转发到所有队列中,对应队列的消费者进行消费

3.5.1 生产者

注意生产者有两个变化:

  1. 声明 Exchange,不再声明 Queue
  2. 发送消息到 Exchange,不再发送到 Queue
public class Send {

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        
        // 声明exchange,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        
        // 消息内容
        String message = "Hello everyone";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

3.5.2 消费者 1

public class Recv {
    private final static String QUEUE_NAME = "fanout_exchange_queue_1";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        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, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.5.3 消费者 2

public class Recv2 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_2";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        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, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.5.4 测试

  1. 运行生产者 Send,这样会生成 exchang。

    在这里插入图片描述

  2. 运行两个消费者 Recv、Recv2

  3. 运行生产者 Send,生产消息

  4. 查看 Recv 运行结果

    在这里插入图片描述

  5. 查看 Recv2 运行结果

    在这里插入图片描述

3.6 路由模型

前面的"发布/订阅模型"会将消息转发到所有队列中消费。但在有些情况下,我们希望不同的消息被不同的队列消费,"路由模型"就可以满足这一需求

在这里插入图片描述

可以看到上面有四个角色:

  • P:生产者,向交换机发送消息时,会指定一个 routing key。
  • X:交换机。接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列。
  • 队列:每个队列绑定到交换机时,需要 routing key。
  • 消费者:每一个消费者都有自己的一个队列。

注意:在路由模型中,交换机的类型为 Direct(定向),它会把消息交给符合指定routing key 的队列

3.6.1 生产者

下我们模拟商品的增加,发送消息的 RoutingKey 是:

  • insert
public class Send {
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "商品新增了, id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

3.6.2 消费者 1

消费者 1 只接收两种类型的消息:

  • update
  • delete
public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_1";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.6.3 消费者 2

消费者 2 接收所有类型的消息:

  • insert
  • update
  • delete
public class Recv2 {
    private final static String QUEUE_NAME = "direct_exchange_queue_2";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的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");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.6.4 测试

  1. 运行生产者 Send

  2. 运行两个消费者 Recv、Recv2

  3. 运行生产者 Send,生产消息

  4. 查看 Recv 运行结果

    在这里插入图片描述

  5. 查看 Recv2 运行结果

    在这里插入图片描述

3.7 主题模型

在这里插入图片描述

主题模型和路由模型都可以根据 RoutingKey 把消息路由到不同的队列,只不过主题模型可以让队列在绑定 Routing key 的时候使用通配符。

注意:在主题模型中,交换机的类型为 Topic(通配符),它会把消息交给符合routing pattern(路由模式) 的队列

通配符规则:

#:匹配一个或多个词

*:匹配一个词

比如上图中:

  • Q1 的路由模式

    *.orange.*
    
  • Q2 的路由模式

    *.*.rabbit
    lazy.#
    

加入生产者发送的消息,会加入队列:

  • quick.orange.rabbit —— Q1、Q2
  • lazy.orange.elephant —— Q1、Q2
  • quick.orange.fox —— Q1
  • lazy.pink.rabbit —— Q2
  • quick.brown.fox —— 无
  • quick.orange.male.rabbit —— 无
  • orange —— 无

3.7.1 生产者

发送消息的 routing key :

  • item.insert
public class Send {
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 消息内容
        String message = "新增商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

3.7.2 消费者 1

消费者 1 只接收两种类型的消息:

  • item.update
  • item.delete
public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.7.3 消费者 2

消费者 2 只接收所有类型的消息:

  • item.insert
  • item.update
  • item.delete
public class Recv2 {
    private final static String QUEUE_NAME = "topic_exchange_queue_2";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

3.7.4 测试

  1. 运行生产者 Send

  2. 运行两个消费者 Recv、Recv2

  3. 运行生产者 Send,生产消息

  4. 查看 Recv 运行结果

    在这里插入图片描述

  5. 查看 Recv2 运行结果

    在这里插入图片描述

3.8 持久化

RabbitMQ 是如何避免消息丢失的呢?

前面我们讲过了 ACK 机制,可以防止消费者丢失消息。

但如果消息还在队列中,这时 RabbitMQ 服务器宕机了呢?此时消息还没到消费者那里

面对这种问题,RabbitMQ 可以对消息进行持久化。要将消息持久化,前提是队列、交换机都持久化。

3.8.1 交换机持久化

在这里插入图片描述

3.8.2 队列持久化

在这里插入图片描述

3.8.3 消息持久化

在这里插入图片描述

3.8.4 持久化的缺点

持久化会将消息保存到硬盘上,这样 RabbitMQ 的效率肯定会受到影响。所以是否使用持久化,取决于系统是更需要安全性还是更需要效率。

4. Spring AMQP

4.1 Spring AMQP 简介

Spring AMQP 项目是用于开发 AMQP 的解决方案。它对 RabbitMQ 底层的一些 API 进行了封装,使其更加的易用,并且提供了很多扩展功能。

4.2 快速入门 Spring AMQP

  1. 创建一个 Maven 工程

  2. 导入依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>cn.itcast.rabbitmq</groupId>
    	<artifactId>itcast-rabbitmq</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.0.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>
    </project>
    
  3. 在 application.yaml 中添加配置

    spring:
      rabbitmq:
        host: 127.0.0.1
        username: leyou
        password: leyou
        virtual-host: /leyou
    
  4. 创建 Listener 类,并创建 listen 方法作为消费者

    @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);
        }
    }
    

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

    • @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:
      • bindings:指定绑定关系,可以有多个。值是 @QueueBinding 的数组,包含下面属性:
        • value:这个消费者关联的队列。值是 @Queue,代表一个队列
        • exchange:队列所绑定的交换机,值是 @Exchange 类型
        • key:队列和交换机绑定的 RoutingKey
  5. 创建测试类,发送消息

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = Application.class)
    public class MqDemo {
    
        @Autowired
        private AmqpTemplate amqpTemplate;
    
        @Test
        public void testSend() throws InterruptedException {
            String msg = "hello, Spring boot amqp";
            this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg);
            // 等待10秒后再结束
            Thread.sleep(10000);
        }
    }
    
  6. 运行测试类,消费者打印消息

    在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bm1998

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

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

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

打赏作者

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

抵扣说明:

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

余额充值