RabbitMQ学习笔记
文章目录
MQ基本概念
MQ的全称为Message Queue,表示消息队列。消息队列是指在消息的传输过程中用来保存消息的容器,主要用于微服务项目中实现各个服务之间的通信(或者说它可以实现分布式系统中各个子系统之间的通信)。
相关说明:①在微服务的通信过程中,发送消息的服务称为生产者,接收消息的服务称为消费者,中间的消息队列又称为中间件;②微服务项目中实现服务之间通信的方式:远程调用直接通信、使用中间件间接通信;③微服务项目也可以称为分布式系统,每个服务就是一个子系统;③MQ相当于是搭建生产者和消费者之间的一座桥梁。
MQ的优点及说明
- 应用解耦:使用MQ中间件可以降低系统之间的耦合性,避免了远程调用过程中出现的问题。生产者发送消息给MQ进行保存,消费者只需要从MQ中获取消息进行消费,降低生产者和消费者之间的一个耦合性,提升了容错性和可维护性
- 异步提速:相比于远程调用,使用MQ中间件在处理用户请求时可以迅速得到响应,提升用户体验和系统吞吐量。
- 削峰填谷:当大量用户并发访问系统时,可以使用MQ中间件来保存这些请求信息作为消息,系统再从MQ中间件获取消息进行处理,也就是让MQ处理这些请求,可以避免服务器因承载不了太多请求而宕机。这样就把请求的峰值削弱,但是大量的请求消息被积压在MQ中而需要系统按照一定速度进行消费,称为削峰填谷。
MQ的缺点及说明
- 降低了系统可用性:引入了MQ外部依赖后必须保证MQ的高可用而不出问题,否则会导致整个分布式系统宕机。
- 提高了系统复杂度:分布式系统引入了MQ实现系统之间的异步调用,提高了系统复杂度的同时也产生了相关的问题:如何保证消息不被重复消费?如何处理消息丢失?如何保证消息传递的顺序。
- 存在一致性问题:当一个子系统给其他多个子系统发送消息时,若存在部分子系统消费失败则会出现消息的不一致。
MQ的使用情况:
- 在微服务项目中,如果一个服务调用其它服务时不需要返回值进行后续处理,那么可以使用MQ。如果调用一个服务的返回值要作为调用另一个服务的请求参数,由于MQ是异步请求多个服务的,因此无法保证调用多个服务的顺序而很有可能出现数据的不一致,这种情况不能使用MQ,使用服务间远程调用即可。
- 如果允许系统数据存在短暂的不一致性,可以使用MQ。当使用了MQ,一个服务在调用另一个服务的过程中,无论是否处理成功,都会直接向用户返回成功结果,这样如果消费者处理出错的话就会存在数据的不一致。如果不允许存在这种数据短暂的不一致性问题,就不要使用MQ。
- 在实现相关业务的过程中,用MQ带来的解耦、提升、削峰的收益远远大于其劣势和使用成本的话,可以使用MQ。
常见的MQ产品
目前市面上比较常见的MQ产品有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ。相关介绍如下
在实际开发中,需要根据业务需求和MQ产品的特征综合考虑再进行选择。
RabbitMQ介绍
RabbitMQ官网地址:http://www.rabbitmq.com/
RabbitMQ是由Rabbit技术公司基于AMQP标准,采用Erlang语言开发的一款消息队列产品,于2007年发布。AMQP的全称为Advanced Message Queuing Protocol,表示高级消息队列协议,在发送和接受消息时需要遵循这一协议,类比HTTP协议。
RabbitMQ基本架构图如下
RabbitMQ工作模式
RabbitMQ工作模式指的是从生产者产生消息到消费者接收消息这一过程的工作方式,主要分为如下6种
-
简单模式(Simple):生产者发送消息给消息队列后,同一个消息队列只能由一个消费者对里面的消息进行消费。
-
工作队列模式(Work Queues):生产者发送消息给消息队列后,同一个消息队列可以由多个消费者对里面的消息进行消费。
相关说明:①工作队列模式的优势:在消息队列中消息过多的情况下,可以采用工作队列模式提高消费消息的速度;②由于多个消费者消费的是同一个队列中的消息,同一条消息只能被一个消费者进行消费,因此它们属于竞争关系;③消费消息的规律:多个消费者消费同一个消息队列中的消息时,会按照消息的顺序分给一定顺序的消费者进行消费,即轮询消费(例如有两个消息1给消费者1进行消费,消息2给消费者2进行消费,消息3给消费者1进行消费,依次类推直到消息被消费完毕);④消息的顺序取决于Channel对象调用basicPublish()方法发送消息的顺序,消费者的顺序取决于Channel对象调用basicConsume()方法消费消息的顺序。
-
发布/订阅模式(Publish/Subscribe):生产者发送消息给交换机后,交换机将消息路由转发给与之绑定的消息队列,然后同一个消息队列只能由一个消费者对里面的消息进行消费。
相关说明:①发布/订阅模式下的交换机类型需要是广播类型(fanout),那么交换机会把消息转发给所有和交换机绑定的消息队列;②交换机只负责接收生产者发送的消息并转发给对应的消息队列,不负责消息的存储,所以交换机的某一条消息如果没有可以转发的消息队列则会丢失;③该模式的特点:只要交换机和消息队列绑定了就会进行转发
-
路由模式(Routing):生产者发送消息给交换机后,交换机将消息路由转发给指定RoutingKey的所有消息队列,然后同一个消息队列只能由一个消费者对里面的消息进行消费。
相关说明:①路由模式下的交换机类型需要是定向类型(direct),交换机和消息队列绑定时需要指定路由Key,发送消息时需要指定路由Key,那么发送消息时交换机会把消息转发给绑定了对应路由Key的消息队列;②该模式的特点:交换机和消息队列不仅要绑定,还要根据绑定的路由Key才会转发到对应的消息队列
-
主题模式(Topics):生产者发送消息给交换机后,交换机将消息路由转发给符合对应路由通配符的所有消息队列,然后同一个消息队列只能由一个消费者对里面的消息进行消费。
相关说明:①主题模式下的交换机类型需要是通配符类型(topic),交换机和消息队列绑定时需要指定路由通配符,发送消息时需要指定路由Key,那么发送消息时交换机会把消息转发给符合对应通配符表达式的消息队列;②主题模式又称之为通配符模式,该模式相对于发布/订阅模式和路由模式来讲,显得更加灵活;③
*
表示一个单词,#
表示0个或多个单词 -
远程调用模式(RPC):
RabbitMQ安装
下面演示一下如何在Linux操作系统中安装RabbitMQ(可以使用Linux虚拟机或Linux云服务器)。
-
使用FinalShell连接工具连接本地的VMware虚拟机,在
/root
目录下创建一个rabbitmq目录。 -
打开当前目录下的
/资料/Rabbit安装
文件夹,将里面的三个rpm文件上传到对应的/root/rabbitmq
目录下。 -
在安装RabbitMQ之前需要确保虚拟机已经安装了相关依赖工具,可以通过如下命令安装
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
-
进入
/root/rabbitmq
目录,根据erlang的rpm文件安装Erlang环境,命令如下rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
-
根据socat的rpm文件安装Socat环境,命令如下
rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps
-
根据rabbitmq的rpm文件安装RabbitMQ服务,命令如下
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
-
为了提供图形化界面进行操作,通过如下命令开启RabbitMQ管理控制台页面。
rabbitmq-plugins enable rabbitmq_management
-
使用vim编辑器修改RabbitMQ的默认配置信息,将默认用户设为
guest
,命令如下vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
-
通过如下命令可以启动、停止、重启RabbitMQ服务以及查看服务的状态,启动服务后管理台页面默认占用15672端口
systemctl start rabbitmq-server # 启动服务 systemctl stop rabbitmq-server # 停止服务 systemctl restart rabbitmq-server # 重启服务 systemctl status rabbitmq-server # 查看服务的状态
-
为了能够在浏览器访问到RabbitMQ管理控制台页面,可以先将虚拟机防火墙关闭,保证端口可以对外放行
systemctl stop firewalld # 关闭防火墙 systemctl disable firewalld # 禁止开机自启动
-
打开浏览器访问
虚拟机网络ip:15672
即可,目前本地虚拟机的网络ip为192.168.19.128
,对应页面如下 -
此时的登录用户名和密码都为
guest
,输入后点击Login按钮进行登录即可,对应页面如下
RabbitMQ管理控制台
下面介绍一下RabbitMQ管理控制台常见的使用操作
-
添加用户的操作
-
添加虚拟机的操作
-
为虚拟机添加用户的操作权限
RabbitMQ入门程序
下面通过代码的方式来演示RabbitMQ中生产者发送消息给消费者进行消费的入门程序(使用简单模式)。
-
打开IDEA开发工具,新建一个空的项目,命名为rabbitmq
-
在这个空项目下新建两个maven项目模块,项目名称分别为
rabbitmq-producer
和rabbitmq-consumer
-
创建后的项目结构如下,
rabbitmq-producer
项目作为生产者项目模块,rabbitmq-consumer
作为消费者项目模块 -
在生产者项目和消费者项目的pom.xml文件中,都添加如下rabbitmq依赖和maven插件并重新加载即可
<dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.6.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
-
在生产者项目的主函数中编写如下代码,然后运行
// 创建rabbitmq服务的连接工厂并设置相关参数(连接的ip、tcp端口、虚拟机名称、用户名、密码) ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.19.128"); factory.setPort(5672); factory.setVirtualHost("/john"); factory.setUsername("john"); factory.setPassword("john"); // 通过连接工厂连接rabbitmq服务,并创建Channel连接对象 Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // 使用channel声明消息队列(没有则创建,否则不创建) channel.queueDeclare("hello_world", true, false, false, null); // 发送消息 String body = "hello~"; channel.basicPublish("", "hello_world", null, body.getBytes()); // 发送完成后释放连接的资源 connection.close(); channel.close();
相关说明:①声明消息队列的queueDeclare()方法中,第一个参数为消息队列的名称、第二个参数为消息是否持久化(即服务重启后消息是否还存在)、第三个参数为是否只允许一个消费者消费、第四个参数为不存在消费者时是否自动删除消息,这一操作可理解成创建消息队列;②发送消息的basicPublish()方法中,第一个参数为交换机名称、第二个参数为绑定的路由Key、第三个参数为相关配置信息、第四个参数为要发送的消息(字节数组),这一操作可理解成在虚拟机中某个交换机通过指定路由分发消息到对应的消息队列中;③发送消息完成后如果不释放资源,那么Connection连接和Channel连接会一直存在,程序并不会终止。
-
打开RabbitMQ管理控制台进行查看,在Queues选项卡中可以看到对应消息队列的相关信息
-
在消费者项目的主函数中编写如下代码,然后运行
// 创建rabbitmq服务的连接工厂并设置相关参数(连接的ip、tcp端口、虚拟机名称、用户名、密码) ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.19.128"); factory.setPort(5672); factory.setVirtualHost("/john"); factory.setUsername("john"); factory.setPassword("john"); // 通过连接工厂连接rabbitmq服务,并创建Channel连接对象 Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // 创建一个DefaultConsumer对象并通过匿名内部类的方式重写handleDelivery()方法 Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { System.out.println("消息编号:" + consumerTag); System.out.println("路由Key:" + envelope.getRoutingKey()); System.out.println("交换机:" + envelope.getExchange()); System.out.println("配置信息:" + consumerTag); System.out.println("消息内容:" + new String(body)); } }; // 对消息队列中的消息进行消费,并且每当有新消息产生则会执行consumer中的handleDelivery()方法 channel.basicConsume("hello_world", true, consumer);
相关说明:①对消息进行消费的basicConsume()方法中,第一个参数为消息队列名称、第二个参数为是否自动确认接收消息、第三个参数为回调对象,这一操作可理解成创建消息队列;②在Consumer对象重写的handleDelivery()方法中,第一个参数为消息编号,第二个参数为虚拟机相关信息,第三个参数为配置信息,第四个参数为消息内容;③消费者项目不能释放掉连接资源,因为消费者需要一直监听消息队列是否有消息,从而进行后续处理,如果释放掉则无法进行消费;
-
控制台打印的结果如下,可以观察到消息队列中的两条消息已被消费,并且消息队列中无任何消息
相关说明:①控制台打印结果的说明:因为生产者发送了两次消息存储到消息队列中,那么消费者会通过调用Channel对象的basicConsume()方法对消息队列中的两条消息进行消费,从而执行两次handleDelivery()方法,而被消费的消息会从消息队列中移除掉(如果是未确认的则不会移除);②
Spring整合RabbitMQ
下面演示一下如何在Spring项目中整合RabbitMQ。
-
打开IDEA开发工具,新建一个空的项目,命名为spring-rabbitmq
-
在这个空项目下新建两个maven项目模块,项目名称分别为
spring-rabbitmq-producer
和spring-rabbitmq-consumer
,分别作为生产者项目和消费者项目。
-
在生产者项目和消费者项目的pom.xml文件中,都添加如下依赖并重新加载即可
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.1.8.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.7.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
-
在
spring-rabbitmq-producer
项目的resources目录下新建一个rabbitmq.properties文件,配置rabbitmq的连接信息;再新建一个spring-rabbitmq-producer.xml文件,作为spring的核心配置文件,相关内容如下rabbitmq.host=192.168.19.128 rabbitmq.port=5672 rabbitmq.username=john rabbitmq.password=john rabbitmq.virtual-host=/john
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!--定义管理交换机、持久化队列,路由key为队列的名称--> <rabbit:admin connection-factory="connectionFactory"/> <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/> <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~广播;所有队列都能收到消息~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义广播交换机中的持久化队列,不存在则自动创建--> <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/> <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/> <!--定义广播类型交换机;并绑定上述两个队列--> <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding queue="spring_fanout_queue_1"/> <rabbit:binding queue="spring_fanout_queue_2"/> </rabbit:bindings> </rabbit:fanout-exchange> <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 路由模式 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义三个消息队列,不存在则自动创建--> <rabbit:queue id="spring_routing_queue1" name="spring_routing_queue1" auto-declare="true"/> <rabbit:queue id="spring_routing_queue2" name="spring_routing_queue2" auto-declare="true"/> <!-- 定义定向路由类型交换机,通过路由模式和消息队列进行绑定并配置路由Key--> <rabbit:direct-exchange id="spring_routing_exchange" name="spring_routing_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding key="query" queue="spring_routing_queue1"/> <rabbit:binding key="delete" queue="spring_routing_queue1"/> <rabbit:binding key="query" queue="spring_routing_queue2"/> </rabbit:bindings> </rabbit:direct-exchange> <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~通配符;*匹配一个单词,#匹配多个单词 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> <!--定义三个消息队列,不存在则自动创建--> <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/> <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/> <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/> <!-- 定义主题类型交换机,通过主题模式和消息队列进行绑定并配置通配符 --> <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true"> <rabbit:bindings> <rabbit:binding pattern="error.*" queue="spring_topic_queue_star"/> <rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/> <rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/> </rabbit:bindings> </rabbit:topic-exchange> <!--定义一个连接RabbitMQ的rabbitTemplate对象并放到spring容器中,在代码中直接依赖注入即可--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/> </beans>
-
在
spring-rabbitmq-producer
项目的java测试目录下新建一个ProducerTest类用于测试,代码如下package com.yonglove; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml") public class ProducerTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testHelloWorld() { // 向spring_queue这一消息队列发送消息(简单模式采用默认交换机) rabbitTemplate.convertAndSend("spring_queue", "hello john"); } @Test public void testPubSub() { // 向spring_fanout_exchange交换机发送消息,分发给对应的消息队列(发布/订阅模式) rabbitTemplate.convertAndSend("spring_fanout_exchange", "", "nihao"); } @Test public void testRouting() { // 向spring_routing_exchange交换机发送消息,传递路由Key后通过路由Key分发给对应的消息队列(路由模式) rabbitTemplate.convertAndSend("spring_routing_exchange", "delete", "this is routing"); } @Test public void testTopic() { // 向spring_topic_exchange交换机发送消息,传递路由Key后根据通配符分发给对应的消息队列(主题模式) rabbitTemplate.convertAndSend("spring_topic_exchange", "error.abc", "查询失败,请重试..."); } }
-
在
spring-rabbitmq-consumer
项目的resources目录下新建一个rabbitmq.properties文件,配置rabbitmq的连接信息;再新建一个spring-rabbitmq-consumer.xml文件,作为spring的核心配置文件,相关内容如下rabbitmq.host=192.168.19.128 rabbitmq.port=5672 rabbitmq.username=john rabbitmq.password=john rabbitmq.virtual-host=/john
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!-- 配置监听器类的bean --> <bean id="springQueueListener" class="com.yonglove.consumer.listener.SpringQueueListener"/> <bean id="fanoutListener1" class="com.yonglove.consumer.listener.FanoutListener1"/> <bean id="fanoutListener2" class="com.yonglove.consumer.listener.FanoutListener2"/> <bean id="routingListener1" class="com.yonglove.consumer.listener.RoutingListener1"/> <bean id="routingListener2" class="com.yonglove.consumer.listener.RoutingListener2"/> <bean id="topicListenerStar" class="com.yonglove.consumer.listener.TopicListenerStar"/> <bean id="topicListenerWell" class="com.yonglove.consumer.listener.TopicListenerWell"/> <bean id="topicListenerWell2" class="com.yonglove.consumer.listener.TopicListenerWell2"/> <!-- 将实例bean和消息队列进行绑定,从而对消息进行消费 --> <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true"> <rabbit:listener ref="springQueueListener" queue-names="spring_queue"/> <rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/> <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/> <rabbit:listener ref="routingListener1" queue-names="spring_routing_queue1"/> <rabbit:listener ref="routingListener2" queue-names="spring_routing_queue2"/> <rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/> <rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/> <rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/> </rabbit:listener-container> </beans>
-
在
spring-rabbitmq-consumer
项目的java主目录下新建一个listener包,在该包下分为新建对应的监听器类,让监听器类实现MessageListener接口并重写onMessage方法,方法参数为所消费的消息,相关代码如下package com.yonglove.consumer.listener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import java.util.Arrays; public class SpringQueueListener implements MessageListener { @Override public void onMessage(Message message) { System.out.println("当前消息:" + Arrays.toString(message.getBody())); } }
相关说明:其中还需要创建等FanoutListener1、FanoutListener2、RoutingListener1、RoutingListener2、TopicListenerStar、TopicListenerWell、TopicListenerWell2等监听器类,相关代码参考上述的SpringQueueListener类即可,也就是说一个类监听一个消息队列。
-
在
spring-rabbitmq-consumer
项目的java测试目录下新建一个ConsumerTest类用于测试,代码如下package com.yonglove.consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml") public class ConsumerTest { @Test public void consumerTest() { // 先加载spring配置文件,再通过死循环保证程序不停止,用于监听消息队列 while (true) { } } }
SpringBoot整合RabbitMQ
下面演示一下如何在SpringBoot项目中整合RabbitMQ。
-
打开IDEA开发工具,新建一个SpringBoot项目,命名为producer-springboot
-
在项目的pom.xml文件中引入如下依赖并重新加载
<!-- rabbitmq依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- springboot测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
-
在项目的resources目录下新建一个application.yml配置文件,配置如下内容
spring: # Spring应用名称 application: name: producer-springboot # 连接rabbitmq服务 rabbitmq: host: 192.168.19.128 port: 5672 username: john password: john virtual-host: /john
-
在项目的主目录下新建config包,然后该包下新建一个RabbitMQConfig类作为配置类,编写如下代码
package com.yonglove.rabbitmq.config; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { public static final String EXCHANGE_NAME = "boot_topic_exchange"; public static final String QUEUE_NAME = "boot_queue"; // 配置交换机并放到spring容器 @Bean("bootExchange") public Exchange bootExchange(){ return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build(); } // 配置消息队列并放到spring容器 @Bean("bootQueue") public Queue bootQueue(){ return QueueBuilder.durable(QUEUE_NAME).build(); } // 根据队列名称和交换机名称从spring容器中注入到方法参数,设置队列和交互机的绑定关系并设置routingKey @Bean public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs(); } }
-
在项目的测试目录下新建一个ProducerTest类用于测试,编写如下代码
package com.yonglove.rabbitmq; import com.yonglove.rabbitmq.config.RabbitMQConfig; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class ProducerTest { // 将连接rabbitmq服务的rabbitTemplate对象注入进来 @Autowired private RabbitTemplate rabbitTemplate; @Test public void send() { // 向交换机发送消息,通过主题模式指定路由Key和消息内容 rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.123", "在Springboot项目中使用RabbitMQ"); } }
-
再新建一个SpringBoot项目,命名为consumer-springboot
-
在项目的pom.xml文件中引入如下依赖并重新加载
<!-- rabbitmq依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- springboot测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
-
在项目的resources目录下新建一个application.yml配置文件,配置如下内容
spring: # Spring应用名称 application: name: consumer-springboot # 连接rabbitmq服务 rabbitmq: host: 192.168.19.128 port: 5672 username: john password: john virtual-host: /john
-
在项目的主目录下新建listener包,然后在该包下新建一个RabbitMQListener类作为监听器类,编写如下代码
package com.yonglove.rabbitmq.listener; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.util.Arrays; @Component public class RabbitMQListener { // 监听boot_queue这一消息队列,一旦消息产生则执行对应方法,而消息会被封装到方法参数中 // 运行项目启动类可以对boot_queue消息队列中的消息进行消费 @RabbitListener(queues = "boot_queue") public void ListenerQueue(Message message) { System.out.println("当前消息:" + Arrays.toString(message.getBody())); } }
RabbitMQ的消息可靠性传递
消息可靠性传递指的是在使用RabbitMQ时,生产者应该将消息成功传递到对应的消息队列中而不出错。在实际的业务场景下,发送消息的生产者有可能会出现消息丢失或者是传递失败的情况,这样就不是可靠性传递。
为了明确消息是否正常传递成功,RabbitMQ提供了两种消息可靠性传递方式
- 确认模式:消息从生产者到交换机会返回一个确认的回调函数(confirmCallback)。
- 退回模式:消息从交换机到消息队列会返回一个退回的回调函数(returnCallback)。
**Spring项目中使用确认模式来传递消息:**在定义rabbitmq连接工厂时,将publisher-confirms属性的值设为true,配置好rabbitTemplate的Bean对象之后,在代码中调用setConfirmCallback()方法并传递一个ConfirmCallback对象,该对象需要重写一个confirm()方法。相关核心代码如下
<!-- 定义rabbitmq连接工厂,设置确认模式(消息是否到达交换机) -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true"/>
<!-- 定义消息队列、交换机、绑定关系等... -->
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testConfirm() {
// 设置确认模式的回调,执行对应的confirm()方法
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("执行了确认模式的回调函数,是否收到?" + b);
}
});
}
// 生产者发送消息
rabbitTemplate.convertAndSend("spring_topic_exchange", "heima.123", "查询失败,请重试...");
}
相关说明:①在上述重写的confirm()方法中,第一个参数为相关配置信息,第二参数为消息是否到达交换机,第三个参数为消息未到达交换机的原因(消息未到达才有值,消息已到达则为null);②由上述代码可知,在确认模式下可以判断生产者发送消息后是否到达了交换机,从而进行后续处理;③无论消息是否到达交换机,上述重写的confirm()方法都会执行
Spring项目中使用退回模式来传递消息:在定义rabbitmq连接工厂时,将publisher-returns属性的值设为true,配置好rabbitTemplate的Bean对象之后,在代码中调用setReturnCallback()方法并传递一个ConfirmCallback对象,该对象需要重写一个confirm()方法,然后交换机还需要设置消息未到达消息队列时的处理方式。相关核心代码如下
<!-- 定义rabbitmq连接工厂,设置退回模式(消息是否到达消息队列) -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-returns="true"/>
<!-- 定义消息队列、交换机、绑定关系等... -->
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
// 设置交换机在消息未到达消息队列时是否返回给生产者进行处理
rabbitTemplate.setMandatory(true);
@Test
public void testConfirm() {
// 设置退回模式的回调,执行对应的setReturnCallback()方法
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
System.out.println("退回模式..对应丢失的消息为;" + message);
}
});
}
// 生产者发送消息(传递一个交换机无法匹配的路由Key)
rabbitTemplate.convertAndSend("spring_topic_exchange", "heima999.abc", "查询失败,请重试...");
}
相关说明:①在上述重写的returnedMessage()方法中,第一个参数为消息对象,第二参数为错误码,第三个参数为错误信息,第四个参数为交换机名称,第五个参数为路由Key;②只有消息没有到达消息队列,上述重写的returnedMessage()方法才会执行,如果到达了消息队列则不会执行;③退回模式下,若消息没有到达消息队列会直接丢失,因此需要设置交换机在消息未到达消息队列时的处理模式为true从而执行returnedMessage()方法;④由上述代码可知,在退回模式下可以确定有哪些消息没有到达消息队列,从而进行后续处理;
Consumer Ack
Consumer Ack指的是Consumer Acknowledge,表示消费者确认模式,它也是消息可靠性传递的一种手段。
消费者确认模式分为如下三种
- 自动确认模式:消费者一旦接收到消息则自动确认,该消息会从消息队列中移除(默认的确认模式)
- 手动确认模式:消费者一旦接收到消息则要通过代码手动确认,确认了之后的消息才会从消息队列中移除
- 根据异常确认模式:根据是否出现异常进行确认,较为复杂而不讲解。
下面以Spring项目为例,用代码实现手动确认模式
-
在Spring项目的核心配置文件中除了加载rabbitmq配置文件并定义连接工厂外,还需要配置组件扫描以及监听器容器。配置组件扫描可以扫描指定包下添加了@Component注解的监听器类并配置为spring中的bean对象,配置监听器容器可以让对应监听器类的bean对象监听指定的消息队列,执行onMessage()方法,同时添加acknowledge属性设为manual值,让消费者手动确认接收消息
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd"> <!--加载配置文件--> <context:property-placeholder location="classpath:rabbitmq.properties"/> <!-- 定义rabbitmq connectionFactory --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}"/> <!-- 配置组件扫描,扫描指定包下添加了@Component注解的监听器类并配置为spring中的bean对象 --> <context:component-scan base-package="com.yonglove.consumer.listener"/> <!-- 配置监听器容器,让对应监听器类的bean对象监听指定的消息队列,执行onMessage()方法 --> <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual"> <rabbit:listener ref="ackListener" queue-names="spring_topic_queue_well"/> </rabbit:listener-container> </beans>
-
在Spring项目的主目录下新建一个listener包,该包下新建一个AckListener类实现ChannelAwareMessageListener接口并重写onMessage()方法,以Message对象和Channel对象作为方法参数;同时添加@Component注解(为了被扫描到)
package com.yonglove.consumer.listener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.stereotype.Component; package com.yonglove.consumer.listener; import com.rabbitmq.client.Channel; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.stereotype.Component; @Component public class AckListener implements ChannelAwareMessageListener { @Override public void onMessage(Message message, Channel channel) throws Exception { // 获取消息的标签 long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { // 获取消息的内容 System.out.println("当前消息:" + new String(message.getBody())); // 此处根据消息处理业务逻辑(中途出现异常则执行catch语句) System.out.println("处理业务逻辑中..."); // 确认接收消息,此时的消息会从消息队列中移除 channel.basicAck(deliveryTag, true); } catch (Exception e) { // 出现异常,拒绝接收消息 channel.basicNack(deliveryTag, true, true); } } }
相关说明:①Channel对象的basicAck()方法表示确认接收消息,第一个参数为消息的标签,第二个参数为是否接收多个消息,②Channel对象的basicNack()方法表示拒绝接收消息,第一个参数为消息的标签,第二个参数为是否接收多个消息,第三个参数为是否将消息返回到消息队列中重新发送(一般要返回,否则消息会丢失);③需要注意一点:如果消费者拒绝接收消息并返回到消息队列中重新发送,在一直出现异常的情况下,消费者会一直拒绝接收消息并返回到消息队列中重新发送,从而陷入一种死循环(此时的消息还会存在消息队列中),除非修改代码直到不出现异常为止。