引用资料:
RabbitMq官方文档API:https://www.rabbitmq.com/api-guide.html
RabbitMq官方之镜像队列:https://www.rabbitmq.com/ha.html
RabbitMq官方文档中文翻译:https://blog.csdn.net/csdnzouqi/article/details/78926603#%E8%AF%91%E6%96%87%E9%93%BE%E6%8E%A5
RabbitMq中文文档:http://rabbitmq.mr-ping.com/AMQP/AMQP_0-9-1_Model_Explained.html
java简单的实现RabbitMQ: https://www.cnblogs.com/goody9807/p/6443749.html
Java实现RabbitMQ:https://www.cnblogs.com/wuzhiyuan/p/6845338.html
RabbitMQ安装:https://blog.csdn.net/vbirdbest/article/details/78573825
RabbitMQ入门教程(一-->十七):https://blog.csdn.net/vbirdbest/article/category/7296570
RabbitMQ消息队列之二:消费者和生产者 Demo:https://blog.csdn.net/qazwsxpcm/article/details/77601358
例1:其中P表示生产者,C表示消费者,红色部分为消息队列
引入依赖:
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> </dependency>
生产者:
package com.example.rabbbitmqDemo.mq; import java.io.IOException; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; /** * @author Aaron 生产者 */ public class Producer { public final static String QUEUE_NAME = "rabbitMQ.test"; public static void main(String[] args) throws IOException, TimeoutException { // 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); // 设置MQ相关信息 factory.setHost("localhost"); factory.setPort(AMQP.PROTOCOL.PORT); // 5672 factory.setUsername("guest"); factory.setPassword("guest"); // 创建一个新的连接 Connection connection = factory.newConnection(); // 创建一个通道 Channel channel = connection.createChannel(); // 声明一个队列 // 注1:queueDeclare第一个参数表示队列名称、第二个参数为是否持久化(true表示是,队列将在服务器重启时生存)、第三个参数为是否是独占队列(创建者可以使用的私有队列,断开后自动删除)、第四个参数为当所有消费者客户端连接断开时是否自动删除队列、第五个参数为队列的其他参数 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 发送消息到队列中 String message = "Hello RabbitMQ"; // 注2:basicPublish第一个参数为交换机名称、第二个参数为队列映射的路由key、第三个参数为消息的其他属性、第四个参数为发送信息的主体 channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8")); System.out.println("Producer Send +'" + message + "'"); // 关闭通道和连接 channel.close(); connection.close(); } }
消费者:
package com.example.rabbbitmqDemo.mq; import java.io.IOException; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; /** * @author Aaron 消费者 */ public class Consumer02 { private final static String QUEUE_NAME = "rabbitMQ.test"; public static void main(String[] args) throws IOException, TimeoutException { // 创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); // 创建连接 Connection connection = factory.newConnection(); // 创建通道 Channel channel = connection.createChannel(); // 设置MQ相关信息 factory.setHost("localhost"); factory.setPort(AMQP.PROTOCOL.PORT); // 5672 factory.setUsername("guest"); factory.setPassword("guest"); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); System.out.println("Customer Waiting Received messages"); // DefaultConsumer类实现了Consumer接口,通过传入一个频道,告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery // 其中envelope主要存放生产者相关信息(比如交换机、路由key等)body是消息实体。 Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); System.out.println("Customer Received '" + message + "'"); } }; // 自动回复队列应答 -- RabbitMQ中的消息确认机制 channel.basicConsume(QUEUE_NAME, true, consumer); } }
结果:
Producer Send +'Hello RabbitMQ'
Customer Received 'Hello RabbitMQ'
Connections and Channels
核心API类是Connection 和Channel,分别表示AMQP 0-9-1连接和通道。这些是使用前需要导入的:
import com.rabbitmq.client.Connection; import com.rabbitmq.client.Channel;
连接到一个代理
下面的代码使用给定的参数(主机名、端口号等)连接到AMQP代理。
ConnectionFactory factory = new ConnectionFactory(); factory.setUsername(userName); factory.setPassword(password); factory.setVirtualHost(virtualHost); factory.setHost(hostName); factory.setPort(portNumber); Connection conn = factory.newConnection();
对于在本地运行的RabbitMQ服务器,所有这些参数都有合理的默认值
或者,也可以使用uri:
ConnectionFactory factory = new ConnectionFactory(); factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost"); Connection conn = factory.newConnection();
所有这些参数对于运行在本地的RabbitMQ服务器都有合理的默认值。
然后可以使用 Connection 接口打开通道:
Channel channel = conn.createChannel();
该通道现在可以用于发送和接收消息,后续部分会有讲解。
要断开连接,只需关闭通道和连接:
channel.close();
conn.close();
请注意,关闭通道可能被认为是良好的习惯,但在这里并不是绝对必要的,因为当底层连接关闭后,任何情况下通道都会自动的关闭
使用 Exchanges and Queues(队列)
客户端应用程序与交换机和队列一起工作,即AMQP的高级构建块。这些必须在使用之前被“声明”。声明任何类型的对象只会确保其中一个名称存在,并在必要时创建它。
继续之前的例子,下面的代码声明了一个交换机和一个队列,然后将它们绑定在一起。
channel.exchangeDeclare(exchangeName, "direct", true); String queueName = channel.queueDeclare().getQueue(); channel.queueBind(queueName, exchangeName, routingKey);
这将主动声明以下的对象,这两个对象都可以通过使用额外的参数进行定制。这里,对象有一些特殊的参数体现:
-
一个持久的、非自动删除的“直接”类型的exchange(交换机)
-
有一个已知名称的、非持久的、专有的、自动删除的队列
上面的函数调用然后用给定的路由键将队列绑定到交换机。
注意,当只有一个客户端希望使用它时,这将是声明队列的一种典型方式:它不需要一个已知的名称,其他客户端不能独占队列,并且队列会自动清理(自动删除)。如果有几个客户端想要共享一个已知名称的队列,那么这段代码将是所需要的:
channel.exchangeDeclare(exchangeName, "direct", true); channel.queueDeclare(queueName, true, false, false, null); channel.queueBind(queueName, exchangeName, routingKey);
这些会主动声明:
-
一个持久的、非自动删除的“直接”类型的exchange(交换机)
-
有一个已知名称的、持久的、非专有的(这里理解为可共享的)、非自动删除的队列
请注意,所有这些 Channel API方法都是重载的。对于exchangeDeclare, queueDeclare 和 queueBind 这些方便的简写形式都是使用了合理的默认。还有更多参数的更长的形式,可以让您根据需要覆盖这些默认值,以便在需要的时候提供绝对控制权。
这种“简写形式、长形式(这里理解为带更多参数的一种形式)”模式在客户端的API中使用。
发布消息(Publishing messages)
要将消息发布到交换机中,请使用 Channel.basicPublish 如下:
byte[] messageBodyBytes = "Hello, world!".getBytes(); channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
为了获得良好的控制,您可以使用重载的变式来指定 mandatory 标志,或者使用预先设置的消息属性来发送消息:
channel.basicPublish(exchangeName, routingKey, mandatory,
MessageProperties.PERSISTENT_TEXT_PLAIN,
messageBodyBytes);
发送一条消息,mode是2(即持久化的),priority 为1,content-type 为 “text/plain”。您可以构建自己的消息属性对象,使用一个Builder类,您可以使用您喜欢的属性,例如:
channel.basicPublish(exchangeName, routingKey, new AMQP.BasicProperties.Builder().contentType("text/plain") .deliveryMode(2) .priority(1).userId("bob") .build()), messageBodyBytes);
举个例子, 发一个带自定义头部的消息:
Map<String, Object> headers = new HashMap<String, Object>(); headers.put("latitude", 51.5252949); headers.put("longitude", -0.0905493);
channel.basicPublish(exchangeName, routingKey, new AMQP.BasicProperties.Builder() .headers(headers) .build()), messageBodyBytes);
这个例子发布了一个expiration的消息:
channel.basicPublish(exchangeName, routingKey, new AMQP.BasicProperties.Builder() .expiration("60000") .build()), messageBodyBytes);
我们还没有说明所有的可能性。
注意,BasicProperties是自动生成的holder类AMQP的内部类。
如果资源驱动的警报生效,那么 Channel#basicPublish 的调用最终会被阻塞。
通道和并发性考虑事项(线程安全)
根据已有经验,在线程之间共享 Channel 实例是可以避免的。应用程序应该选择一个 Channel 对应一个线程,而不是在多个线程之间共享同一个 Channel 。
虽然通道上的一些操作是可以同时调用的,但有些操作是不安全的,并且会导致不正确的帧间交错问题,双确认问题等等。
共享通道上的并发发布可能会导致连接上不正确的帧,从而触发连接级别的协议异常和连接关闭。因此,它需要在应用程序代码中显式同步( Channel#basicPublish 必须在关键部分中调用)。在线程之间共享通道也会干扰发布者的确认。我们强烈建议在共享通道上避免并发发布。
在共享通道时,在一个线程中消费,在另一个线程中发布可以是安全的。
Server-pushed deliveries (见下面的部分)是同时发出的,保证每个通道的顺序被保留。
对每个连接,分派机制使用java.util.concurrent.ExecutorService。可以提供一个自定义执行器,它将由一个ConnectionFactory的ConnectionFactory#setSharedExecutor 调用所产生的所有连接共享。
当使用手动确认时,重要的是要考虑哪些线程做了确认。如果它与接收到的线程不同(例如,Consumer#handleDelivery委托给不同的线程)将 multiple 参数设置为 true 是不安全的,并将导致双重确认,因此会出现通道级协议异常,从而关闭通道。因此一次只确认一条信息可以是安全的。
通过订阅接收消息(”Push API”)
import com.rabbitmq.client.Consumer; import com.rabbitmq.client.DefaultConsumer;
接收消息的最有效方式是使用 Consumer 接口设置订阅。消息将在到达时自动发送,而不用显式地请求。在调用与Consumers有关的API方法时,个人订阅总是与它们的消费者标签相关联。消费者标签是消费者的唯一标识符,它可以是客户端,也可以是服务器生成的。为了让RabbitMQ生成一个节点范围的惟一标记,使用 Channel#basicConsume 方法重写,它不需要携带一个消费者标记参数,或者传递一个空字符串给消费者标记,并使用 Channel#basicConsume 方法返回的值。消费者标签可被用来取消消费者。
不同的消费者实例必须具有不同的消费者标记。对连接上的重复的消费者标签是强烈反对的,当消费者被监控时,会导致自动连接恢复和令人困惑的监控数据的问题。
实现Consumer 的最简单方法是实例化 DefaultConsumer 类。这个子类的一个对象可以通过basicConsume的调用来设置订阅:
boolean autoAck = false; channel.basicConsume(queueName, autoAck, "myConsumerTag", new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String routingKey = envelope.getRoutingKey(); String contentType = properties.getContentType(); long deliveryTag = envelope.getDeliveryTag(); // (这里进行消息组件的处理 ...) channel.basicAck(deliveryTag, false); } });
在这里,因为我们指定了autoAck=false,有必要确认消息传递给消费者,最方便的是在 handleDelivery 方法中完成,如上所示。
更复杂的Consumers需要覆盖更多的方法。特别地,当通道和连接关闭时,handleShutdownSignal被调用,而handleConsumeOk方法会在任何其他回调之前传递消费者标签给Consumer。
消费者还可以实现handleCancelOk 和handleCancel 方法,分别通知显式和隐式取消。
通过消费者标签,你可以用Channel.basicCancel明确地取消一个特定的Consumer :channel.basicCancel(consumerTag);
就像发布者一样,考虑消费者的并发性风险也是很重要的。
对使用者的回调被分派到线程池中,该线程池与实例化Channel的线程分开。这意味着消费者可以安全地调用Connection 或Channel上的阻塞方法,例如 Channel#queueDeclare 或者 Channel#basicCancel。
每个Channel 都有自己的分派线程,对于一个Channel 中一个Consumer 最常见的使用情况,这意味着Consumers不支持其他Consumers。如果一个通道都有多个消费者,那么一个长时间运行的消费者可能会持有该通道上的其他消费者的回调的引用。
关于并发性和并发性危害安全的其他主题,请参阅并发性考虑(线程安全)部分
基本概念
ConnectionFactory(连接工厂): 生产Connection的的工厂
Connection(连接):是RabbitMQ的socket的长链接,它封装了socket协议相关部分逻辑
Channel(频道|信道): 是建立在Connection连接之上的一种轻量级的连接,我们大部分的业务操作是在Channel这个接口中完成的,包括定义队列的声明queueDeclare、交换机的声明exchangeDeclare、队列的绑定queueBind、发布消息basicPublish、消费消息basicConsume等。如果把Connection比作一条光纤电缆的话,那么Channel信道就比作成光纤电缆中的其中一束光纤。一个Connection上可以创建任意数量的Channel。
Producer(生产者):生产者用于发布消息
Exchange(交换机):生产者会将消息发送到交换机,然后交换机通过路由策略(规则)将消息路由到匹配的队列中去
Routing Key(路由键):一个String值,用于定义路由规则,在队列绑定的时候需要指定路由键,在生产者发布消息的时候需要指定路由键,当消息的路由键和队列绑定的路由键匹配时,消息就会发送到该队列。
Queue(队列):用于存储消息的容器,可以看成一个有序的数组,生产者生产的消息会发送到交换机中,最终交换机将消息存储到某个或某些队列中,队列可被消费者订阅,消费者从订阅的队列中获取消息。
Binding(绑定): Binding并不是一个概念,而是一种操作,RabbitMQ中通过绑定,以路由键作为桥梁将Exchange与Queue关联起来(Exchange—>Routing Key—>Queue),这样RabbitMQ就知道如何正确地将消息路由到指定的队列了,通过queueBind方法将Exchange、Routing Key、Queue绑定起来
Consumer(消费者):用于从队列中获取消息,消费者只需关注队列即可,不需要关注交换机和路由键,消费者可以通过basicConsume(订阅模式可以从队列中一直持续的自动的接收消息)或者basicGet(先订阅消息,然后获取单条消息,再然后取消订阅,也就是说basicGet一次只能获取一条消息,如果还想再获取下一条还要再次调用basicGet)来从队列中获取消息
vhost(虚拟主机): RabbitMQ 通过虚拟主机(virtual host)来分发消息, 拥有自己独立的权限控制,不同的vhost之间是隔离的,单独的。vhost是权限控制的基本单位,用户只能访问与之绑定的vhost,默认vhost:”/” ,默认用户”guest” 密码“guest”,来访问默认的vhost。
简单RabbitMQ模型创建: