一、RabbitMQ介绍
1.1 引言
如果客户端要保存客户的数据,就需要通过客户模块去操作。但是我们想保存后在客户模块调用搜索模块,又想调用缓存模块缓存起来,还想调用其它模块。其实我们的核心业务就是用客户模块去存数据,其它的也是必做的,但是不是我们的核心需求。这时候会有什么问题?
- 模块之间的耦合度,导致一个模块宕机后,全部功能都不能用了。比如缓存模块失败,由于现在是串行的,那么客户模块就失败了。
- 同步通讯的成本问题。时间太长了。
这就需要我们的RabbitMQ来帮我们处理了。
1.2 RabbitMQ的介绍
市面上比较火爆的几款MQ:
ActiveMQ(比较古老,比较早)、RocketMQ、Kafka(常用于大数据中)、RabbitMQ。
- 语言的支持:ActiveMQ,RocketMQ只支持Java语言,Kafka可以支持多们语言(最好用java,因为Kafka本身是用什么ktelinna什么的,忘记了),RabbitMQ支持多种语言。
- 效率方面:ActiveMQ,RocketMQ,Kafka效率都是毫秒级别,但是我们的RabbitMQ更加优秀,是微妙级别的。
- 消息丢失,消息重复问题:RabbitMQ针对消息的持久化,和重复问题都有比较成熟的解决方案。(其实其它MQ也有)
- 学习成本:RabbitMQ非常简单,简单到令人发指。
RabbitMQ是由Rabbit公司去研发和维护的,最终在Pivotal。
RabbitMQ严格的遵循AMQP协议,高级消息队列协议,帮助我们在进程之间传递异步消息。这样上面的两个问题都解决了,因为是异步,如果缓存模块宕机,可以不影响用户模块;又因为是异步,时间从成本变小了,用户模块存储完就返回了,只是有另外的线程去执行搜索模块,缓存模块和...模块。
二、RabbitMQ安装
这里安装还是用Daocker安装。下面是docker-compose管理需要的yml文件:
version: "3.1"
services:
rabbitmq:
image: daocloud.io/library/rabbitmq:management # 这里的版本我们要选择后缀有management的,因为我们除了用rabbitmq还要用其图形化界面,这里就选版本是management,不选x.xmanagement
restart: always
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672 # 这个是图形化管理界面的端口号,一定要把他也映射上
volumes:
- ./data:/var/lib/rabbitmq # 我们上面已经说过,rabbitmq会对消息持久化,因此我们用volume映射一下这个数据,方便查看。(为了保证消息不会丢失)
下面我们到xterm中安装一下:
首先进入opt目录下;然后创建docker-rabbitmq文件夹;然后vi docker-compose.yml;然后粘贴上面的yml文件内容,保存;并运行。
下面就可以访问一下,当然我们这里不是访问Rabbit,而是访问rabbitmq的图形化界面:这里有个默认的用户名和密码是guest和guest,这里面的内容等讲Rabbitmq的架构时再讲。
三、RabbitMQ架构
3.1 官方的简单架构图
我这里先大致描述一下下图:这是来自官网的一张图片,我们可以看到最左侧是Publisher,最右侧是Consumer。这两者是没有直接交互的。Publisher是发布消息的,就相当于上面的用户模块,他除了要完成自身的使命保存数据,还要发布消息(调用搜索模块和缓存模块等);而Consumer就是消费消息的,但是这里的消费消息并非是Publisher发布的消息,而是经过处理的消息;这个处理的过程就是RabbitMQ的使命了。我们可以看到RabbitMQ中有两个部分,第一个是Exchange,是与Publish交互的,他接收Publihser发布的消息,然后通过一定的策略转存储到Queue中;而这个Queue和Consumer交互。
- Publisher-生产者:发布消息到RabbitMQ中的Exxchange;
- Consumer-消费者:监听RabbitMQ中的Queue中的消息;
- Exchange-交换机:和生产者建立连接并接收生产者的消息;
- Queue-队列:Exchange会将消息分发到指定的Queue,Queue和消费者进行交互;
- Rotes-路由:交换机以什么样的策略将消息发布到Queue。
上面这幅图可以在RabbitMQ找到;点击Docs;点击AMQP o-9-1 Overview进去即可看到。
这样的话对于上面的问题,我们在客户模块和下面的搜索模块、缓存模块和...模块间用RabbitMQ就可以解决了,即可以解决:一个模块宕机问题;同步通讯成本问题(现在是异步)。
下面我们再讲一下Rabbitmq的详细架构图,我们可以知道上面我们访问的官网显示的都是什么意思。
3.2 RabbitMQ的完整架构图
这个图我来解释一下,其实和上面的是一样的,只是这里展示了如何使用RabbitMQ。
要想使用RabbitMQ,首先我们得有RabbitMQ;然后RabbitMQ里面有许多Vrtual Host,即虚拟机,如果我们点开RabbitMQ的Admin会发现里面有一个Virtual Host叫/,RabbitMQ里面有许多Vitural Host,我们这里用的虚拟机是test,我们需要建;然后每个虚拟机里面有许多Exchange和Queue,他们之间用一定的策略进行转换;生产者和消费者不直接相连,生产者直接和Exchange相连,连接需要先建立连接(和Vitural),再建立通道(和Exchange);对于消费者是和Queue相连的,要想获得消息,也要先建立消费者与Viturl Host的连接,再建立通道(和Queue)。
3.3 查看图形化界面并创建一个Virtual Host
下面的操作是:创建一个全新的用户和全新的Vitural Host,并且将test用户设置上可以操作/test的权限。
这里查看图形化界面并创建一个Virtual Host,以便我们后面对RabbitMQ的使用。
在RabbitMQ的首页,展示的是Overview页面,这个是展示RabbitMQ所有信息的页面,但是不作为我们的重点。
下面我们看一下Connections,里面是no connections,这是没有连接的意思,因为我们还没有建立连接;
channels是管道的意思,现在也是0个;
现在我们看一下Exchanges,虽然我们还没有创建Exchanges,但是已经默认有许多Exchanges了,如果我们发布消息时没有指定哪个Exchange,那么会默认有一个(AMQP),生产者就是把消息发布到Exchange上面的;
下面看队列,也是什么都没有,因为队列Queues是需要我们手动创建的;最后我们可以通过Admin建立前面所说的Vitural Host;现在我们先创建一个用户,进来RabbitMQ默认有一个用户是guest,我们这里再创建一个用户,用户名和密码都叫test,然后Tages是什么意思呢?是说这个用户的权限,其中administrator代表管理员权限,最高权限;
创建好后,我们发现这个用户没有管理任何的Vitural Host(而默认的用户guest管理的Vitural Host是/);下面我们创建一个新的Vitural Host;创建好后,你也可以点击进去管理这个Vitural Host(比如删除);
下面我们让刚刚创建好的用户去管理这个Vitural Host,然后我们就看到test用户管理的就是这个/test的Vitural Host,而guest用户管理所有的vitural Host;后面我们就用test用户下的/test的Vitural Host去管理我们的消息队列;
我们在这个test用户下的/test Vitural Host去建立连接;建立管道;发送消息到Exchanges;再通过一定方式把消息发布到Queues中。此外,我们可以看到,当我们创建了新的Vitural Host'后,他帮我们指定了Exchanges;
现在我们退出guest用户,用test用户登录一波;然后接下来就是操作RabbitMQ,用最传统的Java方式:
四、RabbitMQ的使用
4.1 RabbitMQ的通讯方式
RabbitMQ的通讯方式,我们可以在官网上找到,总共是七种,我们需要重点掌握的是6种:为什么不学RPC模式??我不知道??
怎么找到连接方式?到RabbitMQ官网;点击Docs;找到下面的Fet Started点击;可以看到这七种方式:
4.2 Java连接RabbitMQ
1、 创建Maven项目,这个现在就没啥说的了,我的gav是:g:com.qf;a是rabbitmq;v是默认。
2、导入依赖:这里导入两个依赖,一个是Rabbbitmq,一个是为了测试导入junit;这里的Rabbitmq的artifactId前面有一个amqp,上面我们说了,其遵循该协议。
<?xml version="1.0" encoding="UTF-8"?> <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>com.qf</groupId> <artifactId>rabbitmq</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.6.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> </dependencies> </project>
3、创建工具类连接RabbitMQ。
package com.qf.config; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; public class RabbitMQClient { /* * 其实我们这里直接创建一个Connection对象返回即可,但是呢?我们发现Connection是一个接口,那怎么创建对象呢? * 需要用到Connection的工厂类ConnectionFactory,然后调用这个工厂类ConnectionFactory的方法,newConnection即可; * 但是这时候找你们知道连接哪个RabbitMQ呢?没有主机、端口号、连接的用户名、密码和连接该RabbitMQ中的哪个Vitural Host,因此需要在ConnectionFactory中设置 * */ public static Connection getConnection() { // 创建Connection工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("58.87.106.9"); factory.setPort(5672); // 这个端口是RabbitMQ的端口,而15672是RabbitMQ客户端的端口 factory.setUsername("test"); factory.setPassword("test"); factory.setVirtualHost("/test"); // 创建Connection,有异常,处理一下 Connection conn = null; try { conn = factory.newConnection(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } // 返回 return conn; } }
测试:只要能够正常调用这个方法即可:这里测试在close前面加断点,方便在没有关闭前我们在RabbitMQ客户端的Connections里面看到这个连接,注意要用Debug方式运行。
package com.qf.config; import com.rabbitmq.client.Connection; import java.io.IOException; import org.junit.Test; /* * 这个测试方法要放在test的java包下,但不是说一定要有com.qf.config包,但是这样更加规范 * */ public class RabbitMQClientTest { @Test public void getConnection() throws IOException { Connection connection = RabbitMQClient.getConnection(); // 可以关闭 connection.close(); } }
在没有运行前的RabbitMQ,运行后的RabbitMq客户端,至于这个Name怎么来的,我不清楚,我的IP不是这个呀。
这里连接已经搞定了,下面就要学习上面6种通讯方式了。
4.3 Hello-World
一个生产者,一个默认的交换机,一个队列,一个消费者。
1、创建生产者,发布消息到exchange,指定路由规则
package com.qf.helloworld; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import org.junit.Test; public class Publisher { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2.创建Channel Channel channel = connection.createChannel(); // 3. 发布消息到exchange,同时制定路由规则 String msg = "Hello-World!"; // 参数1:指定exchange,使用""。这样使用默认的exchange // 参数2:指定路由的规则,使用具体的队列名称。这里只是简单的指定路由规则,用的是队列名称 // 参数3: 指定传递的消息所携带的properties,使用null。暂时先不携带参数 // 参数4: 指定发布的具体消息,byte[]类型 channel.basicPublish("","HelloWorld",null,msg.getBytes()); // Ps: exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功"); // 4. 释放资源 channel.close(); connection.close(); } }
2. 创建消费者,创建一个channel,创建一个队列,并且去消费当前队列
package com.qf.helloworld; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld // 参数1: queue - 指定队列的名称 // 参数2: durable - 当前队列是否需要持久化(true),这样的话RabbitMQ宕机或重启,该队列依然存在 // 参数3: exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) // 参数4: autoDelete - 如果这个队列没有消费者在消费,队列自动删除 // 参数5: arguments - 指定当前队列的其他信息 channel.queueDeclare("HelloWorld",true,false,false,null); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到的消息:" + new String(body, "UTF-8")); } }; // 参数1: queue - 指定消费哪个队列 // 参数2“ autoAck - 指定是否自动ACK(true,接收到消息后,会立即告诉RabbitMQ) // 参数3: consumer - 指定消费回调 channel.basicConsume("HelloWorld",true,consume); System.out.println("消费者开始剪影消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
测试的时候要先运行消费者,再运行生产者。另外,在RabbitMQ页面看一看到这个消息被消费了。
4.4 Work
一个生产者,一个默认的交换机,一个队列,两个消费者。
1、创建生产者,发布消息到exchange,指定路由规则(这里和上面基本一样,几乎没有改变,只是指定的路由规则变了;另外,为了测出2个消费者的效果,我们这里发布多条消息)
package com.qf.work; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publisher { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3. 发布消息到exchang,同时指定路由规则(这里的队列就不用HelloWorld了,前面在用了。)还有由于这里是两个消费者,为了看到效果,我们这里多发一些消息。 for (int i = 0; i < 10; i++) { String msg = "Hello-World!" + i; channel.basicPublish("","Work",null,msg.getBytes()); } // 4、 释放资源 channel.close(); connection.close(); } }
2. 创建消费者,创建一个channel,创建一个队列,并且去消费当前队列
消费者1
package com.qf.work; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("Work",true,false,false,null); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者1号接收到的消息:" + new String(body, "UTF-8")); } }; channel.basicConsume("Work",true,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
消费者2
package com.qf.work; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer2 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("Work",true,false,false,null); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { System.out.println("消费者2号接收到的消息:" + new String(body, "UTF-8")); } }; channel.basicConsume("Work",true,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
测试:先运行消费者,再运行生产者,在控制台,我们发现两个消费者是轮流消费消息的。
这样就有问题,如果两个消费者的消费能力不一样,比如1消费的强一点,而2消费的弱一点,那该怎么办?我们需要大改一下:基于上面问题,我们让消费者根据自己的能力去队列中拉取指定数量的数据进行消费,消费完,直接消费下一个,不用等待他给你平均分配。为了实现这个效果,我们大改一下:
1、之前我们是自动ACK的,即自动的告诉哦队列我这个消息消费完了,现在改成手动。
2、其次指定当前消费者每次可以消费多少数据。
消费者1
package com.qf.work; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel final Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("Work",true,false,false,null); // 3.1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException {
// 这里让线程休眠100ms,我没有复制,你要写一下 System.out.println("消费者1号接收到的消息:" + new String(body, "UTF-8")); // 手动ACK channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume("Work",false,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
消费者2:消费者2号也要添加一样的信息
package com.qf.work; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel final Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("Work",true,false,false,null); // 3.1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者1号接收到的消息:" + new String(body, "UTF-8")); // 手动ACK channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume("Work",false,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
4.5 Publish/Subscribe
一个生产者,一个交换机,两个队列,两个消费者
声明一个Fanout类型的exchange,并且将exchange和queue绑定在一起,绑定的方式就是直接绑定。修改如下:
1、让生产者创建一个exchange并且指定类型,和一个或多个队列绑定到一起。
package com.qf.pubsub; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publisher { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3、创建exchange - 绑定某一个队列(这个绑定队列的操作可以在生产者这块操作,也可以在消费者那块操作) // 参数1: exchange的名称 // 参数2: 指定exchange的类型 FANOUT - pubsub,DIRECT - routing,TOPIC - Topics channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT); channel.queueBind("pubsub-queue1","pubsub-exchange",""); // 最后的""代表没有指定特殊的绑定规则,就是名字 channel.queueBind("pubsub-queue2","pubsub-exchange",""); // 最后的""代表没有指定特殊的绑定规则,就是名字 // 3. 发布消息到exchang,同时指定路由规则(这里的队列就不用HelloWorld了,前面在用了。)还有由于这里是两个消费者,为了看到效果,我们这里多发一些消息。 for (int i = 0; i < 10; i++) { String msg = "Hello-World!" + i; channel.basicPublish("pubsub-exchange","Work",null,msg.getBytes()); // 此时生产者给Exchange发消息,已经不是默认的exchange了,此外我们看到这个routingKey完全没有改,这是没有影响的,因为本来就不用写,""即可 } // 4、 释放资源 channel.close(); connection.close(); } }
2、消费者不做改变,还是正产管道监听某一个队列即可。
package com.qf.pubsub; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel final Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("pubsub-queue1",true,false,false,null); // 3.1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者1号接收到的消息:" + new String(body, "UTF-8")); // 手动ACK channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume("pubsub-queue1",false,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
消费者2代码略;测试先启动两个消费者,再运行生产者,发现两个消费者都消费10条数据,这是为什么呢?我也不清楚,但是先记住,此时生产者发布10条消息给exchange,两个队列都获得这10条消息。
4.6 Routing
一个生产者,一个交换机,两个队列,两个消费者
创建一个DIRECT类型的exchange,并且去根据RoutingKey去绑定指定的队列。
1、生产者DIRECT类型的exchange后,去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可。
package com.qf.routing; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publisher { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3、创建exchange - 绑routing-queue-error,routing-queue-info channel.exchangeDeclare("routing-exchange",BuiltinExchangeType.DIRECT); channel.queueBind("routing-queue-error","routing-exchange","ERROR"); channel.queueBind("routing-queue-info","routing-exchange","INFO"); // 3. 发布消息到exchang,同时指定路由规则。这里发布不同类型的消息 channel.basicPublish("routing-exchange","ERROR",null,"ERROR".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO1".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO2".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO3".getBytes()); System.out.println("生产者发布消息成功!"); // 4、 释放资源 channel.close(); connection.close(); } }
2、消费者基本没有变化,还是基本监听自己的队列。
package com.qf.routing; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel final Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("routing-queue-error",true,false,false,null); // 3.1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者ERROR接收到的消息:" + new String(body, "UTF-8")); // 手动ACK channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume("routing-queue-error",false,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
消费者2代码略。
4.7 Topic
一个生产者,一个交换机,两个队列,两个消费者。(和上面不同的只是Exchange和队列绑定的方式不同)
1、生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式xxx.xxx.xxx去编写,*->一个xxx,而#->代表多个xxx.xxx,在附送消息时,指定具体的RoutingKey到底是什么。
package com.qf.topic; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publisher { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 创建exchange绑定队列 // 动物的信息<speed> <color> <what> // *.fast.# -> * 占位符 // fast.# -> # 通配符 // *.*.rabbit channel.exchangeDeclare("topic-exchange",BuiltinExchangeType.TOPIC); channel.queueBind("topic-queue-1","topic-exchange","*.red.*"); channel.queueBind("topic-queue-2","topic-exchange","fast.#"); channel.queueBind("topic-queue-2","topic-exchange","*.*.rabbit"); // 3. 发布消息到exchang,同时指定路由规则(这里的队列就不用HelloWorld了,前面在用了。)还有由于这里是两个消费者,为了看到效果,我们这里多发一些消息。 channel.basicPublish("topic-exchange","fast.red.monkey",null,"红快猴子".getBytes()); channel.basicPublish("topic-exchange","slow.black.dog",null,"黑慢狗".getBytes()); channel.basicPublish("topic-exchange","fast.white.cat",null,"快白猫".getBytes()); System.out.println("生产者发布消息成功!"); // 4、 释放资源 channel.close(); connection.close(); } }
2、消费者只是监听队列,没有变化。
总共有两个队列,这里只写一个,第二个是"对快的和兔子感兴趣接收到的消息"
package com.qf.topic; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import java.io.IOException; import org.junit.Test; public class Consumer1 { @Test public void consume() throws Exception { // 1. 获取连接对象 Connection connection = RabbitMQClient.getConnection(); // 2. 创建channel final Channel channel = connection.createChannel(); // 3. 声明队列-HelloWorld channel.queueDeclare("topic-queue-1",true,false,false,null); // 3.1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); // 4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("对红色动物感兴趣接收到的消息:" + new String(body, "UTF-8")); // 手动ACK channel.basicAck(envelope.getDeliveryTag(),false); } }; channel.basicConsume("topic-queue-1",false,consume); System.out.println("消费者开始消费消息"); // System.in.read(); 避免程序一运行就终止,这里让有键盘录入再终止 System.in.read(); // 5. 释放资源 channel.close(); connection.close(); } }
五、RabbitMQ整合SpringBoot
5.1 SpringBoot整合RabbitMQ
1、创建SpringBoot工程:这里是创建SPringBoot工程,名字是springboot-rabbitmq,直接导入Spring Web依赖,让然我们也可以在Messaging中选中Spring for RabbitMQ,这里不选了,我们手动加入这个依赖。依赖文件如下
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qf</groupId> <artifactId>springboot-rabbitmq</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-rabbitmq</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2、导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.4.1</version> </dependency>
3、编写配置文件
spring:
rabbitmq:
host: 58.87.106.9
port: 5672
username: test
password: test
virtual-host: /test
4、编写配置类,声明exchange和queue,并且绑定到一起
package com.qf.springbootrabbitmq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { // 1. 创建exchange - topic @Bean public TopicExchange getTopicExchange() { return new TopicExchange("boot-topic-exchange", true, false); } // 2. 创建Queue @Bean public Queue getQueue() { return new Queue("boot-queue",true,false,false,null); } // 3. 绑定在一起 @Bean public Binding getBinding(TopicExchange topicExchange, Queue queue) { return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*"); } }
5、发布消息到RabbitMQ(这一步就是准备生产者)
package com.qf.springbootrabbitmq; 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 class SpringbootRabbitmqApplicationTests { @Autowired private RabbitTemplate rabbitTemplate; @Test void contextLoads() { rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!"); } }
6.创建消费者监听消息
package com.qf.springbootrabbitmq.listen; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class Consumer { @RabbitListener(queues = "boot-queue") public void getMessage(Object message) { System.out.println("接收到消息:"+message); } }
测试:
我们先注释掉消费者这个组件,运行一下测试类,此时我们看到RabbitMQ界面的exchange里面有刚刚发布的消息,当然queue里面没有数据。
当我们不注释消费组件,可以看到queue的界面也收到了消息,当然控制台也打印了。
5.2 手动Ack
我们要在消息消费完之后手动告诉RabbitMQ,这个消息我消费完了。
1、添加配置文件,本来默认是自动,这里写成手动
spring:
rabbitmq:
host: 58.87.106.9
port: 5672
username: test
password: test
virtual-host: /test
listener:
simple:
acknowledge-mode: manual
2、手动Ack,非常简单,原本我们是用Object接收了所有信息,这里分三个参数接收,然后手动Ack。
package com.qf.springbootrabbitmq.listen; import com.rabbitmq.client.Channel; import java.io.IOException; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class Consumer { @RabbitListener(queues = "boot-queue") public void getMessage(String msg, Channel channel, Message message) throws IOException { System.out.println("接收到消息:"+message); // 手动Ack,第二个参数false,代表这里不是批量操作 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } }
我们为什么一定要手动Ack,而不是自动?因为如果是自动的话,RabbitMQ进入这个消费方法即自动Ack了,但是可能我们的逻辑报错了,怎么办?因此我们要处理完逻辑后手动Ack。
六、RabbitMQ的其他操作
6.1 消息的可靠性
正如下图中所说:如果消息已经到达了RabbitMQ,但是RabbitMQ宕机了,消息是不是就丢了?RabbitMQ得到Queue有持久化机制。我们重启RabbitMQ,这个数据依然会回到队列;如果消费者在消费时,如果执行一般,消费者宕机了怎么办?我们可以用手动Ack解决;但是如果生产者发送消息时,如果遇到网络问题,是导致消息没有发送到RabbitMQ?RabbitMQ提供了事务操作和Confirm操作来解决这个问题。对于事务操作,效率低下,直接Pass,对于Confirm确认机制,有以下三种方式:
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。事务的操作,效率太低,加了事务操作后,比平时的操作效率至少慢100倍(我们上面已经降到RabbitMQ速度非常快,是微秒级别的)。这个Pass掉。
6.1.1 Confirm机制
RabbitMQ除了事务,还提供了Confirm的确认机制,这个效率比事务高很多。
- 普通Confirm方式。
- 批量Confirm方式。
- 异步Confirm方式。
下面我们建一个包confirm(注意,这个不是在Spring-boot项目了,是本博客的第一个项目rabbitmq),里面还是生产者和消费者
对于普通的Confirm方式的生产者,非常简单。
package com.qf.confirm; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publish { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3. 发布消息到Exchange,同时指定路由的规则 // 3.1 开启confirm channel.confirmSelect(); String msg = "Hello-World!"; channel.basicPublish("","HelloWorld",null,msg.getBytes()); // 的判断消息是否发送成功 if (channel.waitForConfirms()) { System.out.println("消息发送成功"); } else { // 这个失败的地方(如网络异常),我们可以直接抛出异常,可以写到数据库,再发一次,或者其他。这就是业务了 System.out.println("发送消息失败"); } // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息 System.out.println("生产者发布消息成功!"); // 4. 释放资源 channel.close(); connection.close(); } }
测试:
消息发送成功
生产者发布消息成功!
控制台打印成功,这里失败不好测试,我们就不测了。
下面是针对批量操作,即批量Confirm方式,那就需要发送多条数据了。(这个还是无法测试,因为只有出现一些特殊问题时,才会抛出异常)
package com.qf.confirm; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import org.junit.Test; public class Publish { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3. 发布消息到Exchange,同时指定路由的规则 // 3.1 开启confirm
channel.confirmSelect(); // 3.2 批量发送消息 for (int i = 0; i < 1000; i++) { String msg = "Hello-World!" + i; channel.basicPublish("","HelloWorld",null,msg.getBytes()); } // 3.3 确定批量操作是否成功 channel.waitForConfirmsOrDie(); // 当你发送的全部消息,有一个失败的时候,就直接全部失败,抛出异常IOException // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息 System.out.println("生产者发布消息成功!"); // 4. 释放资源 channel.close(); connection.close(); } }
下面这个就是我们最常用的异步Confirm方式。对于这种方式,confirm在确认消息是否发布成功时,是采用异步的机制,也就是说,这里会开启多线程,在进入if-else内部前,主程序就会执行下面的System.out.println("生产者发布消息成功!");如果直接测试,由于主程序已经结束,但这个异步需要时间,因此会有可能没有输出confirm的结果,程序就结束了。因此后面我们加System.in.read();进行等待,不让程序结束,这样可以看到Confirm的结果。只是我运行的结果和老师的不一样,这里我也不找原因了。老师的是否皮批量都是false。
package com.qf.confirm; import com.qf.config.RabbitMQClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmListener; import com.rabbitmq.client.Connection; import java.io.IOException; import org.junit.Test; public class Publish { @Test public void publish() throws Exception { // 1. 获取Connection Connection connection = RabbitMQClient.getConnection(); // 2. 创建Channel Channel channel = connection.createChannel(); // 3. 发布消息到Exchange,同时指定路由的规则 // 3.1 开启confirm channel.confirmSelect(); // 3.2 开启异步回调 for (int i = 0; i < 1000; i++) { String msg = "Hello-World!" + i; channel.basicPublish("","HelloWorld",null,msg.getBytes()); } // 3.3 开启异步回调 channel.addConfirmListener(new ConfirmListener() { public void handleAck(long l, boolean b) throws IOException { System.out.println("消息发送成功,标识:"+l+",是否是批量"+b); } public void handleNack(long l, boolean b) throws IOException { System.out.println("消息发送失败,标识:"+l+",是否是批量"+b); } }); // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息 System.out.println("生产者发布消息成功!"); // 4. 释放资源 channel.close(); connection.close(); } }
测试:控制台为:
生产者发布消息成功!
消息发送成功,标识:550,是否是批量true
在上面java代码的System.out.println("生产者发布消息成功!");前面加System.in.read();再次测试:发现控制台打印了多条,这里我的结果和老师的不一样。不知道为什么,今天好累,就不深究了。
6.1.2 Return机制
Confirm只能保证消息到exchange,无法保证消息可以被exchange分发到指定queue。
而且exchange是不会持久化消息的,queue是可以持久化消息。
采用Return机制来监听消息是否从exchange送到了指定的queue中。
我的