RabbitMQ概念与五种消息模型

一.rabbitMQ概述

1.1搜索与商品服务的问题

假设我们已经完成了商品详情和搜索系统的开发。我们思考一下,是否存在问题?

商品的原始数据保存在数据库中,增删改查都在数据库中完成。

搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据能否及时更新。

如果我们在后台修改了商品的价格,搜索页面依然是旧的价格,这样显然不对。该如何解决?

方案1:每当后台对商品做增删改操作,同时要修改索引库数据

在这里插入图片描述

方案2:搜索服务对外提供操作接口,后台在商品增删改后,调用接口

在这里插入图片描述

以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则。

所以,我们会通过另外一种方式来解决这个问题:消息队列

在这里插入图片描述

1.2消息队列

1.2.1什么是消息队列

消息队列,即MQMessage Queue

“消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。
消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。

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

结合前面所说的问题:

· 商品服务对商品增删改以后,无需去操作索引库,只是发送一条消息,也不关心消息被谁接收。

· 搜索服务接收消息,去处理索引库。

如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。

1.2.2AMQP和JMS

MQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。

两者间的区别和联系:

· JMS是定义了统一接口,对消息操作进行统一;AMQP通过规定协议统一数据交互的格式;

· JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的

· JMS规定了两种消息模型(queue, topic);而AMQP的消息模型更加丰富

1.2.3常见MQ产品

· ActiveMQ:基于JMS

· RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

· RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会

· Kafka:分布式消息系统,高吞吐量,处理日志,Scala和Java编写,Apache

1.2.4 RabbitMQ

官网: http://www.rabbitmq.com/

官方教程:http://www.rabbitmq.com/getstarted.html

1.2.5 MQ 三大主要功能

异步

解耦

削峰

1.2.6 RabbitMQ工作模型

在这里插入图片描述

1.Broker

我们要使用RabbitMQ来收发消息,必须要安装个RabbitMQ的服务,可以安装在Windows上面也可以安装在Linux 上面,默认是5672的端口。这台RabbitMQ的服务器我们把它叫做 Broker, MQ 服务器帮助我们做的事情就是存储、转发消息。

2.Connection

无论是生产者发送消息,还是消费者接收消息,都必须要跟 Broker 之间建立一个连接,这个连接是一个 TCP 的长连接。

3.Channel

如果所有的生产者发送消息和消费者接收消息,都直接创建和释放 TCP 长连接的话,对于 Broker 来说肯定会造成很大的性能损耗,因为 TCP 连接是非常宝贵的资源,创建和释放也要消耗时间。所以在 AMQP 里面引入了 Channel 的概念,它是一个虚拟的连接。这样我们就可以在保持的 TCP 长连接里面去创建和释放Channel,大大了减少了资源消耗。

4.Queue

队列是真正用来存储消息的,是一个独立运行的进程,有自己的数据库(Mnesia)。

我们可以基于事件机制,实现消费者对队列的监听。

由于队列有 FIFO 的特性,只有确定前一条消息被消费者接收之后,才会把这条消息从数据库删除,继续投递下一条消息。

5.Exchange

在 RabbitMQ 里面永远不会出现消息直接发送到队列的情况。因为在 AMQP 里面引入了交换机(Exchange)的概念,用来实现消息的灵活路由。

交换机是一个绑定列表,用来查找匹配的绑定关系。

队列使用绑定键(Binding Key)跟交换机建立绑定关系。

生产者发送的消息需要携带路由键(Routing Key),交换机收到消息时会根据它保存的绑定列表,决定将消息路由到哪些与它绑定的队列上。

注意:交换机与队列、队列与消费者都是多对多的关系。

6.Vhost

VHOST 除了可以提高硬件资源的利用率之外,还可以实现资源的隔离和权限的控制。

不同的 VHOST 中可以有同名的 Exchange 和 Queue,它们是完全透明的。

这个时候,我们可以为不同的业务系统创建不同的用户(User),然后给这些用户分配 VHOST 的权限。

1.2.7 使用rabbitMQ会带来的一些问题?

系统可用性降低:原来是两个节点的通信,现在还需要独立运行一个服务,如果 MQ服务器或者通信网络出现问题,就会导致请求失败。

系统复杂性提高: 为什么说复杂?第一个就是你必须要理解相关的模型和概念,才能正确地配置和使用 MQ。第二个,使用 MQ 发送消息必须要考虑消息丢失和消息重复消费的问题。一旦消息没有被正确地消费,就会带来数据一致性的问题。

所以,我们在做系统架构的时候一定要根据实际情况来分析,不要因为我们说了这么多的 MQ 能解决的问题,就盲目地引入 MQ。

1.3下载和安装

1.3.1 下载

RabbitMQ是Erlang语言编写,所以Erang环境必须要有,注:Erlang环境一定要与RabbitMQ版本匹配:https://www.rabbitmq.com/which-erlang.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ldfemveq-1601995129490)(img/snipaste_20200605_133434.jpg)]

Erlang下载地址:https://www.rabbitmq.com/releases/erlang/(根据自身需求及匹配关系,下载对应rpm包)

https://dl.bintray.com/rabbitmq-erlang/rpm/erlang/21/el/7/x86_64/erlang-21.3.8.9-1.el7.x86_64.rpm

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

socat下载地址:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm

RabbitMQ下载地址:https://www.rabbitmq.com/download.html(根据自身需求及匹配关系,下载对应rpm包)rabbitmq-server-3.8.1-1.el7.noarch.rpm

1.3.2安装

在这里插入图片描述

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
rpm -ivh rabbitmq-server-3.8.1-1.el7.noarch.rpm 

启用管理插件

rabbitmq-plugins enable rabbitmq_management

启动RabbitMQ

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

查看进程

ps -ef | grep rabbitmq

在这里插入图片描述
启用延时队列插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange
1.3.3 测试

o 关闭防火墙:systemctl stop firewalld.service

o 在web浏览器中输入地址:http://虚拟机ip:15672/

o 输入默认账号密码:guest : guest,guest用户默认不允许远程连接。

添加用户

rabbitmqctl add_user root root

分配角色

rabbitmqctl set_user_tags root administrator

修改密码

rabbitmqctl change_password root root

查看所有用户

rabbitmqctl list_users
1.3.4卸载
	rpm -qa | grep rabbitmq
	rpm -e rabbitmq-server

1.4管理界面

1.4.1 添加用户

在这里插入图片描述

1、 超级管理员(administrator)

可登录管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

2、 监控者(monitoring)

可登录管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

3、 策略制定者(policymaker)

可登录管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

4、 普通管理者(management)

仅可登录管理控制台,无法看到节点信息,也无法对策略进行管理。

5、 其他

无法登录管理控制台,通常就是普通的生产者和消费者。

1.4.2 创建 Virtual Hosts

虚拟主机:类似于mysql中的database。他们都是以“/”开头

在这里插入图片描述

1.4.3设置权限

二.五种消息模型

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

在这里插入图片描述

创建项目演示

pom文件

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.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>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.4.3</version>
        </dependency>
    </dependencies>

抽取一个获取连接的工具类

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

    public static void main(String[] args) throws Exception {
        Connection con = ConnectionUtil.getConnection();
        System.out.println(con);
        con.close();
    }

}

2.1基本消息模型

RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。

在这里插入图片描述

/**
 * 生产者
 */
public class Send {

    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);
        // 消息内容
        String message = "Hello World!";
        // 向指定的队列中发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        
        System.out.println(" [x] Sent '" + message + "'");

        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
/**
 * 消费者
 */
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, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
消息确认机制(ACK)

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

· 不过这种回执ACK分两种情况:

o 自动ACK:消息一旦被接收,消费者自动发送ACK

o 手动ACK:消息接收后,不会发送ACK,需要手动调用

· 大家觉得哪种更好呢?

· 这需要看消息的重要性:

o 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便

o 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。

我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码

/**
 * 消费者,手动进行ACK
 */
public class Recv2 {
    private final static String QUEUE_NAME = "simple_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);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                //int i = 1/0 ;
                System.out.println(" [x] received : " + msg + "!");
                // 手动进行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列,第二个参数false,手动进行ACK
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

2.2 work 消息模型

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

在这里插入图片描述

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

面试题:避免消息堆积?

1)采用workqueue,多个消费者监听同一队列。

2)接收到消息以后,而是通过线程池,异步消费。

// 生产者
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();
    }
}
// 消费者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, AMQP.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);
    }
}
//消费者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, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
                try {
                    // 模拟完成任务的耗时:1000ms
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                }
                // 手动ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

与消费者1基本类似,就是没有设置消费耗时时间。

这里是模拟有些消费者快,有些比较慢。

接下来,两个消费者一同启动,然后发送50条消息:

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

能者多劳

现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。

· 怎么实现呢?

o 我们可以使用basicQos方法和prefetchCount = 1设置。

o 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。

o 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。

o 相反,它会将其分派给不是仍然忙碌的下一个工作人员。

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

2.3订阅模型分类

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

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

在这里插入图片描述

1、1个生产者,多个消费者

2、每一个消费者都有自己的一个队列

3、生产者没有将消息直接发送到队列,而是发送到了交换机

4、每个队列都要绑定到交换机

5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的

X(Exchanges)

交换机一方面:接收生产者发送的消息。

另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

Exchange类型有以下几种:

o Fanout:广播,将消息交给所有绑定到交换机的队列

o Direct:定向,把消息交给符合指定routing key 的队列

o Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

2.4订阅模型-Fanout

也称为广播。

在广播模式下,消息发送流程是这样的

1) 可以有多个消费者

2) 每个消费者有自己的queue(队列)

3) 每个队列都要绑定到Exchange(交换机)

4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。

5) 交换机把消息发送给绑定过的所有队列

6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

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();
    }
}
//消费者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, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
// 消费者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, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

2.5 订阅模型-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、warning 的消息

此处我们模拟商品的增删改,发送消息的RoutingKey分别是:insert、update、delete

/**
 * 生产者,模拟为商品服务
 */
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, "delete", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
/**
 * 消费者1
 */
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);
    }
}
/**
 * 消费者2
 */
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);
    }
}

2.6订阅模型-Topic

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

· Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

· 通配符规则:

`#`:匹配**0或多个词**(含零个)

`*`:匹配不多不少恰好1个词(不含零个)

· 举例:

`audit.#`:能够匹配`audit.irs.corporate` 或者 `audit.irs`

`audit.*`:只能匹配`audit.irs`

在这里插入图片描述

· 在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:“..****”。

· 我们创建三个绑定:

Q1绑定了绑定键“* .orange.”,Q2绑定了“.*.rabbit”和“lazy.#”。

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

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

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

​ o quick.orange.rabbit Q1 Q2

​ o lazy.orange.elephant Q1 Q2

​ o quick.orange.fox Q1

​ o lazy.pink.rabbit Q2

​ o quick.brown.fox 都不能

​ o quick.orange.male.rabbit 都不能

​ o orange 都不能

/**
 * 生产者,模拟为商品服务
 */
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",true);

        // 消息内容
        String message = "更新商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.update", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}
/**
 * 消费者1
 */
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, true, 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, AMQP.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);
    }
}
/**
 * 消费者2
 */
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, AMQP.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);
    }
}

2.7持久化

· 如何避免消息丢失?

1) 消费者的ACK机制。可以防止消费者丢失消息。

2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。

· 如何将消息进行持久化呢?

要将消息持久化,前提是:队列、Exchange都持久化

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

在这里插入图片描述

2.7.2队列持久化

在这里插入图片描述


三,Spring AMQP

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

3.1依赖

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

3.2配置

spring:
  rabbitmq:
    host: 121.199.31.160
    username: root
    password: root
    virtual-host: /shopping
    port: 5672

3.3监听者

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

@Component
public class Listener {

    @RabbitListener(bindings = @QueueBinding(
        //value:队列名
            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:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:

​ o bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:

​ o value:这个消费者关联的队列。值是@Queue,代表一个队列

​ o exchange:队列所绑定的交换机,值是@Exchange类型

​ o key:队列和交换机绑定的RoutingKey

​ durable = “true” 代表持久化

​ ignoreDeclarationExceptions = “true”, 异常情况是否忽略

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

3.4AmqpTemplate

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

在这里插入图片描述

红框圈起来的是比较常用的3个方法,分别是:

o 指定消息

o 指定RoutingKey和消息,会向默认的交换机发送消息

o 指定交换机、RoutingKey和消息体

测试代码:

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

收到消息:" + msg);
}
}


@Componet:类上的注解,注册到Spring容器

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

​			o  bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:

​			o  value:这个消费者关联的队列。值是@Queue,代表一个队列

​			o  exchange:队列所绑定的交换机,值是@Exchange类型

​			o  key:队列和交换机绑定的RoutingKey

​			durable = "true" 代表持久化

​			ignoreDeclarationExceptions = "true", 异常情况是否忽略

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

3.4AmqpTemplate

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

在这里插入图片描述

红框圈起来的是比较常用的3个方法,分别是:

o 指定消息

o 指定RoutingKey和消息,会向默认的交换机发送消息

o 指定交换机、RoutingKey和消息体

测试代码:

@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);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值