RabbitMQ

异步消息的定义

异步消息的主要目的是为了系统与系统之间的通信,所谓异步消息即消息发送者无需等待消息接收者的处理以及返回,甚至无需关心消息是否发送成功
  在异步消息中有两个很重要的概念,即消息代理和目的地,当消息发送者发送消息之后,消息将由消息代理接管,消息代理保证消息传递到指定目的地。
  异步消息主要有两种目的地形式,队列(queue)和主题(topic),队列用于点对点形式的消息通信,主题用于发布订阅式的消息通信。

点对点式

当消息发送者发送消息,消息代理将消息后将消息放进一个队列里,当有消息接收者来接收消息的时候,消息将从队列中取出传递给消息接收者,这时候队列里就没有了这条消息。点对点式确保每一条消息只有唯一的发送者和接收者,但这并不能说明只有一个接收者能够从队列中接收消息,因为队列中有多个消息,点对点式只保证每一条消息只有唯一的发送者和接收者

发布/订阅式

发布订阅式是消息发送者发送消息到主题,而多个消息接收者监听这个主题,此时的消息发送者和接收者分别叫做发布者和订阅者

JMS

通常而言提到JMS(Java MessageService)实际上是指JMS API。JMS是由Sun公司早期提出的消息标准,旨在为java应用提供统一的消息操作,包括create、send、receive等。JMS已经成为Java Enterprise Edition的一部分。从使用角度看,JMS和JDBC担任差不多的角色,用户都是根据相应的接口可以和实现了JMS的服务进行通信,进行相关的操作。
JMS。
JMS提供了两种消息模型,peer-2-peer(点对点)以及publish-subscribe(发布订阅)模型。当采用点对点模型时,消息将发送到一个队列,该队列的消息只能被一个消费者消费。而采用发布订阅模型时,消息可以被多个消费者消费。在发布订阅模型中,生产者和消费者完全独立,不需要感知对方的存在。

消息如何从producer端达到consumer端由message-routing来决定。在JMS中,消息路由非常简单,由producer和consumer链接到同一个queue(p2p)或者topic(pub/sub)来实现消息的路由。JMSconsumer同时支持message selector(消息选择器),通过消息选择器,consumer可以只消费那些通过了selector筛选的消息。在JMS兄中,消息路由机制的图示如下:
在这里插入图片描述

常见的消息队列,大部分都实现了JMS API,可以担任JMS provider的角色,如ActiveMQ,Redis以及RabbitMQ等。

AMQP

AMQP(advanced message queuing protocol)在2003年时被提出,最早用于解决金融领不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。意味着我们可以使用Java的AMQP provider,同时使用一个python的producer加一个rubby的consumer。从这一点看,AQMP可以用http来进行类比,不关心实现的语言,只要大家都按照相应的数据格式去发送报文请求,不同语言的client均可以和不同语言的server链接。

在AMQP中,消息路由(messagerouting)和JMS存在一些差别,在AMQP中增加了Exchange和binding的角色。producer将消息发送给Exchange,binding决定Exchange的消息应该发送到那个queue,而consumer直接从queue中消费消息。queue和exchange的bind有consumer来决定。AMQP的routing scheme图示过程如下:
在这里插入图片描述

目前AMQP逐渐成为消息队列的一个标准协议,当前比较流行的rabbitmq、stormmq都使用了AMQP实现。

JMS和AMQP的各项对比如下:

项目JMSAMQP
定义Java apiWire-protocol
跨语言
跨语言
Model提供两种消息模型:
Peer-2-Peer,
Pub/sub
提供了五种消息模型:
direct exchange,
fanout exchange,
topic change,
headers exchange,
system exchange。
本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;
支持消息类型多种消息类型:
TextMessage,
MapMessage,
BytesMessage,
StreamMessage,
ObjectMessage,
Message (只有消息头和属性)
byte[]
当实际应用时,有复杂的消息,可以将消息序列化后发送。
综合评价JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语言特性。

基础概念

ConnectionFactory、Connection、Channel

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。
Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。

Queue

Queue(队列)是RabbitMQ的内部对象,用于存储消息,用下图表示:

Queue name

RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费。

在这里插入图片描述

多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

在这里插入图片描述

Message acknowledgment

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;
如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑。另外pub message是没有ack的。

Message durability

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。

Prefetch count

前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。

在这里插入图片描述

Exchange

在上一节我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(如果没有Queue与之绑定,则消息会被丢弃)。

在这里插入图片描述

Exchange是按照什么逻辑将消息路由到Queue的?这个将在Binding一节介绍。
RabbitMQ中的Exchange有四种类型,不同的类型有着不同的路由策略,这将在Exchange Types一节介绍。

routing key

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。
在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。
RabbitMQ为routing key设定的长度限制为255 bytes。

Binding

RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。

在这里插入图片描述

Binding key

在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。这个将在Exchange Types章节会列举实际的例子加以说明。
在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。
binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。

Exchange Types

RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述),下面分别进行介绍。

fanout

fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

在这里插入图片描述

上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。

direct

direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。

在这里插入图片描述

以上图的配置为例,我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。

topic

前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:
routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
binding key与routing key一样也是句点号“. ”分隔的字符串。
binding key中可以存在两种特殊字符“”与“#”,用于做模糊匹配。其中“”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)

在这里插入图片描述

以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1与Q2,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。

headers

headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地),所以不做介绍。

RPC

MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。

在这里插入图片描述

RabbitMQ中实现RPC的机制是:
客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)。服务器端收到消息并处理,服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性。客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理。

VirtualHost

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。

客户端amqp-client

通过amqp-client来操作RabbitMQ,在pom文件引入如下依赖即可。

 <dependency> 
	 <groupId>com.rabbitmq</groupId> 
	 <artifactId>amqp-client</artifactId> 
	 <version>3.4.1</version> 
 </dependency>

编写连接代码

//定义连接工厂 
ConnectionFactory factory = new ConnectionFactory(); 
//设置服务地址 
factory.setHost("127.0.0.1"); 
//端口 
factory.setPort(5672); 
//设置rabbitmq服务器vhost 
factory.setVirtualHost("/enjoyshop"); 
//设置rabbitmq服务器用户名
factory.setUsername("enjoyshop"); 
//设置rabbitmq服务器密码
factory.setPassword("enjoyshop"); 
//通过工程获取连接 
Connection connection = factory.newConnection();
//创建一个通道
Channel channel = conn.createChannel();
/**
* 声明一个 exchange.
* @param exchange 名称
* @param type  exchange type:direct、fanout、topic、headers
* @param durable 持久化
* @param autoDelete 是否自动删除(没有连接自动删除)
* @param arguments 队列的其他属性(构造参数)
* @return 成功地声明了一个声明确认方法来指示交换。
* @throws java.io.IOException if an error is encountered
*/
channel.exchangeDeclare("leitao","topic", true,false,null);

/**
* 申明一个队列,如果这个队列不存在,将会被创建
* 此方法一般由Producer调用创建消息队列。如果由Consumer创建队列,有可能Producer发布消息的时候Queue还没有被创建好,会造成消息丢失的情况。
* @param queue 队列名称
* @param durable 持久性:true队列会再重启过后存在,但是其中的消息不会存在。
* @param exclusive 是否只能由创建者使用,其他连接不能使用。
* @param autoDelete 是否自动删除(没有连接自动删除)
* @param arguments 队列的其他属性(构造参数)
* @return Queue.DeclareOk:宣告队列的声明确认方法已成功声明。
* @throws java.io.IOException if an error is encountered
*/
channel.queueDeclare(queueName , true, false, false, null);
/**
* 将队列绑定到Exchange,不需要额外的参数。
* @param queue 队列名称
* @param exchange 交换机名称
* @param routingKey 路由关键字
* @return Queue.BindOk:如果成功创建绑定,则返回绑定确认方法。
* @throws java.io.IOException if an error is encountered
*/
channel.queueBind(queueName, exchangeName, routingKey);

string message = "Hello, world!";

/**
* 发布一条不用持久化的消息,且设置两个监听。
* @param exchange 消息交换机名称,空字符串将使用直接交换器模式,发送到默认的Exchange=amq.direct。此状态下,RoutingKey默认和Queue名称相同
* @param routingKey 路由关键字
* @param mandatory 监听是否有符合的队列,当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返回给生产者(Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。
* @param BasicProperties  设置消息持久化:MessageProperties.PERSISTENT_TEXT_PLAIN是持久化;MessageProperties.TEXT_PLAIN是非持久化。
* @param body 消息对象转换的byte[]
* @throws java.io.IOException if an error is encountered
*/
channel.basicPublish("",queueName,true,MessageProperties.TEXT_PLAIN,SerializationUtils.serialize(message ));

/**
* 设置消费批量投递数目,一次性投递10条消息。当消费者未确认消息累计达到10条时,rabbitMQ将不会向此Channel上的消费者投递消息,直到未确认数小于10条再投递
* @param prefetchCount 投递数目
* @param global 是否针对整个Channel。true表示此投递数是给Channel设置的,false是给Channel上的Consumer设置的。
* @throws java.io.IOException if an error is encountered
*/
channel.basicQos(10,false);
//整个传输管道最多15条,具体分到每个消费者身上又不能大于10条
channel.basicQos(15,true);
 
/**
* 开始一个非局部、非排他性消费, with a server-generated consumerTag.
* 执行这个方法会回调handleConsumeOk方法
* @param queue 队列名称
* @param autoAck 是否自动应答。false表示consumer在成功消费过后必须要手动回复一下服务器,如果不回复,服务器就将认为此条消息消费失败,继续分发给其他consumer。
* @param callback 回调方法类,一般为自己的Consumer类
* @return 由服务器生成的consumertag
* @throws java.io.IOException if an error is encountered
*/
channel.basicConsume(queueName, false, Consumer);

/**
* 消费者收到消息的回调函数
* @param consumerTag 消费者标签
* @param envelope 消息的包装数据
* @param properties 消息的内容头数据
* @param body 消息对象的byte[]
* @throws IOException
*/
void handleDelivery(String consumerTag,
                        Envelope envelope,
                        AMQP.BasicProperties properties,
                        byte[] body)
        throws IOException;        

Channel是线程安全的,但是最好是每个线程里用自己的Channel,因为在单个Channel里排队是有可能慢一些的。

生产者confirm模式

没有消息确认模式时,生产者不知道消息是不是已经到达了Broker服务器,这对于一些业务严谨的系统来说将是灾难性的。消息确认模式可以采用AMQP协议层面提供的事务机制实现(此文没有这种实现方式),但是会降低RabbitMQ的吞吐量。RabbitMQ自身提供了一种更加高效的实现方式:confirm模式。

消息生产者通过调用Channel.confirmSelect()方法将Channel信道设置成confirm模式。一旦信道被设置成confirm模式,该信道上的所有消息都会被指派一个唯一的ID(从1开始),一旦消息被对应的Exchange接收,Broker就会发送一个确认给生产者(其中deliveryTag就是此唯一的ID),这样消息生产者就知道消息已经成功到达Broker。

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。

在channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm又被nack 。

注意:已经在transaction事务模式的channel是不能再设置成confirm模式的,即这两种模式是不能共存的。

普通confirm模式是串行的,即每次发送了一次消息,生产者都要等待Broker的确认消息,然后根据确认标记权衡消息重发还是继续发下一条。由于是串行的,在效率上是比较低下的。

/**
* 等待Broker返回消息确认标记
* 注意,在非确定的通道,waitforconfirms抛出IllegalStateException。
* @return 是否发送成功
* @throws java.lang.IllegalStateException
*/
boolean waitForConfirms() throws InterruptedException;

//注意:返回的时候Return在前,Confirm在后
channel.confirmSelect();
int i=1;
while (i<=50) {
	//发布消息
	channel.basicPublish("",queueName,true,MessageProperties.TEXT_PLAIN,SerializationUtils.serialize(object));
	//等待Broker的确认回调
	if(channel.waitForConfirms())
		System.out.println("send success!");
	else
		System.out.println("send error!");
	i++;
}

生产者批量confirm模式

批量confirm模式是异步的方式,效率要比普通confirm模式高许多,但是此种方式也会造成线程阻塞,想要进行失败重发就必须要捕获异常。网络上还有采用waitForConfirms()实现批量confirm模式的,但是只要一条失败了,就必须把这批次的消息统统再重发一次,非常的消耗性能,因此此文不予考虑。

/**
* 等待直到所有消息被确认或者某个消息发送失败。如果消息发送确认失败了,
* waitForConfirmsOrDie 会抛出IOException异常。当在非确认通道上调用时
* ,会抛出IllegalStateException异常。
* @throws java.lang.IllegalStateException
*/
void waitForConfirmsOrDie() throws IOException, InterruptedException;

//注意:返回的时候Return在前,Confirm在后
channel.confirmSelect();
int i=1;
while (i<=50) {
	//发布消息
	channel.basicPublish("",queueName,true,MessageProperties.TEXT_PLAIN,SerializationUtils.serialize(object));
	i++;
}
channel.waitForConfirmsOrDie();
ConfirmListener监听器模式

RabbitMQ提供了一个ConfirmListener接口专门用来进行确认监听,我们可以实现ConfirmListener接口来创建自己的消息确认监听。ConfirmListener接口中包含两个回调方法:

/**
* 生产者发送消息到exchange成功的回调方法
* 其中deliveryTag是Broker给每条消息指定的唯一ID(从1开始);multiple表示是否接收所有的应答消息,比如multiple=true时,发送100条消息成功过后,我们并不会收到100次handleAck方法调用。
*/
void handleAck(long deliveryTag, boolean multiple) throws IOException;
/**
* 生产者发送消息到服务器broker失败的回调方法,服务器丢失了此消息。
* 注意,丢失的消息仍然可以传递给消费者,但broker不能保证这一点。
*/
void handleNack(long deliveryTag, boolean multiple) throws IOException;
//注意:返回的时候Return在前,Confirm在后
channel.confirmSelect();
//注册消息确认监听器
channel.addConfirmListener(new MyConfirmListener());
//注册消息结果返回监听器
channel.addReturnListener(new MyReturnListener());
int i=1;
while (i<=50) {
    //发布消息
    channel.basicPublish("",queueName,true,MessageProperties.TEXT_PLAIN,SerializationUtils.
    serialize(object));
    i++;
}
//自定义的消息确认监听器
public class MyConfirmListener implements ConfirmListener{
	/**
	 * 生产者发送消息到exchange成功的回调方法
	 * 消息被Exchange接受以后,如果没有匹配的Queue,则会被丢弃。但是可以设置ReturnListener监听来监听有没有匹配的队列。
	 * 因此handleAck执行了,并不能完全表示消息已经进入了对应的队列,只能表示对应的exchange成功的接收了消息。
	 * 消息被exchange接收过后,还需要通过一定的匹配规则分发到对应的队列queue中。
	 */
	public void handleAck(long deliveryTag, boolean multiple) throws IOException {
		//注意:deliveryTag是broker给消息指定的唯一id(从1开始)
		System.out.println("Exchange接收消息:"+deliveryTag+"(deliveryTag)成功!multiple="+multiple);
	}
	/**
	 * 生产者发送消息到服务器broker失败的回调方法,服务器丢失了此消息。
	 * 注意,丢失的消息仍然可以传递给消费者,但broker不能保证这一点。(不明白,既然丢失了,为啥还能发送)
	 */
	public void handleNack(long deliveryTag, boolean multiple) throws IOException {
		System.out.println("Exchange接收消息:"+deliveryTag+"(deliveryTag)失败!服务器broker丢失了消息");
	}
}

//自定义的结果返回监听器
/**
 * 实现此接口以通知交付basicpublish失败时,“mandatory”或“immediate”的标志监听(源代码注释翻译)。
 * 在发布消息时设置mandatory等于true,监听消息是否有相匹配的队列,
 * 没有时ReturnListener将执行handleReturn方法,消息将返给发送者 
 */
public class MyReturnListener implements ReturnListener {
	public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
			BasicProperties properties, byte[] body) throws IOException {
		System.out.println("消息发送到队列失败:回复失败编码:"+replyCode+";回复失败文本:"+replyText+";失败消息对象:"+SerializationUtils.deserialize(body));
	}
}
Consumer消息确认机制

为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(message acknowledgment)。消费者在注册消费者时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。

当noAck=false时,对于RabbitMQ服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息(web管理界面上的Ready状态);一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息(web管理界面上的Unacked状态)。如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

/**
 *开始一个非局部、非排他性消费, with a server-generated consumerTag.
 * 注意:执行这个方法会回调handleConsumeOk方法,在此方法中处理消息。
 * @param queue 队列名称
 * @param autoAck 是否自动应答。false表示consumer在成功消费过后必须要手动回复一下服务器,如果不回复,服务器就将认为此条消息消费失败,继续分发给其他consumer。
 * @param callback 回调方法类
 * @return 由服务器生成的consumertag
 * @throws java.io.IOException if an error is encountered
 */
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
 
/**
 * consumer处理成功后,通知broker删除队列中的消息,如果设置multiple=true,表示支持批量确认机制以减少网络流量。
 * 例如:有值为5,6,7,8 deliveryTag的投递,如果此时channel.basicAck(8, true);则表示前面未确认的5,6,7投递也一起确认处理完毕。
 * 如果此时channel.basicAck(8, false);则仅表示deliveryTag=8的消息已经成功处理。
 */
void basicAck(long deliveryTag, boolean multiple) throws IOException;
 
/**
 * consumer处理失败后,例如:有值为5,6,7,8 deliveryTag的投递。
 * 如果channel.basicNack(8, true, true);表示deliveryTag=8之前未确认的消息都处理失败且将这些消息重新放回队列中。
 * 如果channel.basicNack(8, true, false);表示deliveryTag=8之前未确认的消息都处理失败且将这些消息直接丢弃。
 * 如果channel.basicNack(8, false, true);表示deliveryTag=8的消息处理失败且将该消息重新放回队列。
 * 如果channel.basicNack(8, false, false);表示deliveryTag=8的消息处理失败且将该消息直接丢弃。
 */
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
 
/**
 * 相比channel.basicNack,除了没有multiple批量确认机制之外,其他语义完全一样。
 * 如果channel.basicReject(8, true);表示deliveryTag=8的消息处理失败且将该消息重新放回队列。
 * 如果channel.basicReject(8, false);表示deliveryTag=8的消息处理失败且将该消息直接丢弃。
 */
void basicReject(long deliveryTag, boolean requeue) throws IOException;
//this表示自己的Consumer
channel.basicConsume(queueName, false, this);
...
@Override
public void handleDelivery(String arg0, Envelope envelope, BasicProperties arg2, byte[] body) throws IOException {
		if (body == null)
			return;
		Map<String, Object> map = (Map<String, Object>) SerializationUtils.deserialize(body);
/**
 		* 专门处理奇数消息的消费者
 */
		int tagId = (Integer) map.get("tagId");
		if (tagId % 2 != 0) {
			//处理消息
			System.out.println("接收并处理消息:"+tagId);
			//通知服务器此消息已经被处理了
			channel.basicAck(envelope.getDeliveryTag(), false);
		}else{
			//通知服务器消息处理失败,重新放回队列。false表示处理失败消息不放会队列,直接删除
			channel.basicReject(envelope.getDeliveryTag(), true);
		}
	}

工作模式

简单队列模式(点对点)

在这里插入图片描述
P:消息的生产者
C:消息的消费者
红色:队列
生产者将消息发送到队列,消费者从队列中获取消息。

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

生产者发送消息到队列:

//简单模式 
public class Send { 
	private final static String QUEUE_NAME = "test_queue"; 
	public static void main(String[] argv) throws Exception { 
		// 获取到连接以及mq通道 
		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 = "test_queue"; 
	public static void main(String[] argv) throws Exception { 
		// 获取到连接以及mq通道 
		Connection connection = ConnectionUtil.getConnection(); 
		Channel channel = connection.createChannel(); 
		// 声明队列,不存在则创建新队列,若存在则忽略该声明 
		channel.queueDeclare(QUEUE_NAME, false, false, false, null); 
		// 定义队列的消费者 
		QueueingConsumer consumer = new QueueingConsumer(channel); 
		// 监听队列 
		channel.basicConsume(QUEUE_NAME, true, consumer); 
		// 获取消息 
		while (true) { 
			QueueingConsumer.Delivery delivery = consumer.nextDelivery(); 
			String message = new String(delivery.getBody()); 
			System.out.println(" [x] Received '" + message + "'"); 
		} 
	} 
}

Work模式(点对点)

在这里插入图片描述
这里有一个生产者、2个消费者。
一个消息只能被一个消费者获取。

消费者1

//work模式
public class Recv {

    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 同一时刻服务器只会发一条消息给消费者(即使得能者多劳)
        //channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            // 休眠
            Thread.sleep(10);
            // 返回确认状态
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2

//work模式 
public class Recv2 { 
	private final static String QUEUE_NAME = "test_queue_work"; 
	public static void main(String[] argv) throws Exception { 
		// 获取到连接以及mq通道 
		Connection connection = ConnectionUtil.getConnection(); 
		Channel channel = connection.createChannel(); 
		// 声明队列 
		channel.queueDeclare(QUEUE_NAME, false, false, false, null); 
		// 同一时刻服务器只会发一条消息给消费者,(即使得能者多劳) 
		// channel.basicQos(1); 
		// 定义队列的消费者 
		QueueingConsumer consumer = new QueueingConsumer(channel); 
		// 监听队列,手动返回完成状态 
		channel.basicConsume(QUEUE_NAME, false, consumer); 
		// 获取消息 
		while (true) { 
			QueueingConsumer.Delivery delivery = consumer.nextDelivery(); 
			String message = new String(delivery.getBody()); 
			System.out.println(" [x] Received '" + message + "'"); 
			// 休眠1秒 
			Thread.sleep(1000); 
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); 
		} 
	} 
}

生产者

//work模式
public class Send {

    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        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 = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i * 10);
        }

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

程序运行结果:

  • 消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。
  • 消费者1和消费者2获取到的消息的数量是相同的,一个是奇数一个是偶数。

这样是不合理的,因为消费者1的休眠间隔更短,应该是消费者1要比消费者2获取到的消息多合理。

  • Work模式的“能者多劳”

如果要实现上述功能,可以在每个消费者中加入如下代码:

        // 加入这一行代码使得同一时刻服务器只会发一条消息给消费者(即能者多劳)
        channel.basicQos(1);

这样再运行代码的话,消费者1接受的消息数就比消费者2的数量要多。

  • 消息的确认模式

消费者从队列中获取消息,服务端如何知道消息已经被消费呢?

模式1:自动确认
只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。可以通过如下代码设置:

// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);

basicConsume方法的第二个参数设为true,即表示采用自动确认模式。

模式2:手动确认
消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。可以通过如下代码设置:

// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 返回确认状态        
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

订阅模式(fanout,属于发布订阅)

在这里插入图片描述

特点:

  1. 1个生产者,多个消费者。
  2. 每一个消费者都有自己的一个队列。
  3. 生产者没有将消息直接发送到队列,而是发送到了交换机
  4. 每个队列都要绑定到交换机。
  5. 生产者发送的消息,经过交换机,到达队列,实现一个消息被多个消费者获取的目的。

消息的生产者

//订阅模式
public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

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

注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。

消费者1

//订阅模式
public class Recv {

    private final static String QUEUE_NAME = "test_queue_fanout_1";

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2

//订阅模式
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_fanout_2";

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

路由模式(direct,属于发布订阅)

在这里插入图片描述

生产者

//路由模式
public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "key2", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

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

消费者1

//路由模式
public class Recv {

    private final static String QUEUE_NAME = "test_queue_work";

    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key");
        //当需要绑定多个key值时,可以多次执行绑定语句
        //channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key3");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2

//路由模式
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_work2";

    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key2");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

测试结果:

只有消费者2接收到了消息,因为消费者2和生产者有相应的关键字绑定。

通配符模式(topic,属于发布订阅)

在这里插入图片描述
生产者

//通配符模式
public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "key.1", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

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

消费者1

//通配符模式
public class Recv {

    private final static String QUEUE_NAME = "test_queue_topic_work";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

消费者2

//通配符模式
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_topic_work2";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

Spring集成RabbitMQ

Spring针对JMS和RabbitMQ分别提供了JmsTemplete和RabbitTemplete来发送消息。为我们提供了@JmsListener,@RabbitListener注解来监听消息代理发送的消息。我们分别需要通过@EnableJms和@EnableRabbit来开启支持

  1. 引入依赖
 <dependency> 
	 <groupId>org.springframework.amqp</groupId> 
	 <artifactId>spring-rabbit</artifactId> 
	 <version>1.4.0.RELEASE</version> 
 </dependency>
  1. 定义消费者
public class Foo {

    //具体执行业务的方法
    public void listen(String foo) {
        System.out.println("消费者: " + foo);
    }
}
  1. 配置文件
<!--采用<rabbit/>方式配置连接-->
<rabbit:connection-factory id="connectionFactory" 
host="${rabbitmq.host}" 
port="${rabbitmq.port}" 
        username="${rabbitmq.username}" 
        password="${rabbitmq.password}" 
        virtual-host=""
        publisher-confirms="true"
        publisher-returns="true"
        channel-cache-size="5"
        />
<!--采用普通方式配置连接-->
<bean id="connectionFactory"
	class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
		<constructor-arg value="${rabbitmq.host}" />
		<property name="username" value="${rabbitmq.username}" />
		<property name="password" value="${rabbitmq.password}" />
		<property name="port" value="${rabbitmq.port}" />
		<!-- 缓存中要维护的通道数 -->
		<property name="channelCacheSize" value="5" />
		<!-- 开启发送确认机制 -->
		<property name="publisherConfirms" value="true"/>
		<!-- 开启结果返回机制 -->
		<property name="publisherReturns" value="true"/>
	</bean>     

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
	xsi:schemaLocation="http://www.springframework.org/schema/rabbit
	http://www.springframework.org/schema/rabbit/spring-rabbit-1.4.xsd
	http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

	<!-- 定义RabbitMQ的连接工厂 -->
	<rabbit:connection-factory id="connectionFactory"
		host="127.0.0.1" port="5672" username="enjoyshop" password="enjoyshop"
		virtual-host="/enjoyshop" />

	<!-- 定义Rabbit模板,指定连接工厂以及定义exchange -->
	<rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="fanoutExchange" />
	<!-- 加入routing-key的设置 -->
	<!-- <rabbit:template id="amqpTemplate" connection-factory="connectionFactory"
		exchange="fanoutExchange" routing-key="foo.bar" /> -->

	<!-- MQ的管理,包括队列、交换器等 -->
	<rabbit:admin connection-factory="connectionFactory" />

	<!-- 定义队列,自动声明 -->
	<rabbit:queue name="myQueue" auto-declare="true"/>
	
	<!-- 定义交换器,自动声明 -->
	<rabbit:fanout-exchange name="fanoutExchange" auto-declare="true">
		<!-- 绑定队列 -->
		<rabbit:bindings>
			<rabbit:binding queue="myQueue"/>
		</rabbit:bindings>
	</rabbit:fanout-exchange>
	
	<!-- 指定路由交换机 -->
<!-- 	<rabbit:topic-exchange name="myExchange">
		<rabbit:bindings>
			<rabbit:binding queue="myQueue" pattern="foo.*" />
		</rabbit:bindings>
	</rabbit:topic-exchange> -->

	<!-- 队列监听 -->
	<rabbit:listener-container connection-factory="connectionFactory">
		<rabbit:listener ref="foo" method="listen" queue-names="myQueue" />
	</rabbit:listener-container>

   <!-- 声明需要接收消息的类 -->
	<bean id="foo" class="com.kang.rabbitmq.spring.Foo" />

</beans>
  1. 测试方法
public class SpringMain {
    public static void main(final String... args) throws Exception {
        AbstractApplicationContext ctx = new ClassPathXmlApplicationContext(
                "classpath:spring/rabbitmq-context.xml");
        //RabbitMQ模板
        RabbitTemplate template = ctx.getBean(RabbitTemplate.class);
        //发送消息
        template.convertAndSend("Hello, world!");
        Thread.sleep(1000);// 休眠1秒
        ctx.destroy(); //容器销毁
    }
}

SpringBoot集成RabbitMQ

SpringBoot自动配置了上述@EnableJms,@EnableRabbit,JmsTemplete,RabbitTemplete的支持,同时我们可以在application.properties文件中分别以spring.activemq和spring.rabbitmq来分别配置所需的属性。

  1. 引入依赖
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置文件
spring.application.name=spirng-boot-rabbitmq
spring.rabbitmq.host=192.168.0.86
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456

简单模式

队列配置

@Configuration
public class RabbitConfig {
 
    @Bean
    public Queue Queue() {
        return new Queue("hello");
    }
 
}

发送者

public class HelloSender {
 
    @Autowired
    private AmqpTemplate rabbitTemplate;
 
    public void send() {
        String context = "hello " + new Date();
        System.out.println("Sender : " + context);
        this.rabbitTemplate.convertAndSend("hello", context);
    }
 
}

接收者

@Component
@RabbitListener(queues = "hello")
public class HelloReceiver {
 
    @RabbitHandler
    public void process(String hello) {
        System.out.println("Receiver  : " + hello);
    }
 
}

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitMqHelloTest {
 
    @Autowired
    private HelloSender helloSender;
 
    @Test
    public void hello() throws Exception {
        helloSender.send();
    }
 
}

Topic Exchange

@Configuration
public class TopicRabbitConfig {
 
    final static String message = "topic.message";
    final static String messages = "topic.messages";
 
    @Bean
    public Queue queueMessage() {
        return new Queue(TopicRabbitConfig.message);
    }
 
    @Bean
    public Queue queueMessages() {
        return new Queue(TopicRabbitConfig.messages);
    }
 
    @Bean
    TopicExchange exchange() {
        return new TopicExchange("exchange");
    }
 
    @Bean
    Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
    }
 
    @Bean
    Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
    }
}
public void send1() {
    String context = "hi, i am message 1";
    System.out.println("Sender : " + context);
    this.rabbitTemplate.convertAndSend("exchange", "topic.message", context);
}
 
public void send2() {
    String context = "hi, i am messages 2";
    System.out.println("Sender : " + context);
    this.rabbitTemplate.convertAndSend("exchange", "topic.messages", context);
}
}

发送send1会匹配到topic.#和topic.message 两个Receiver都可以收到消息,发送send2只有topic.#可以匹配所有只有Receiver2监听到消息

Fanout Exchange

@Configuration
public class FanoutRabbitConfig {
 
    @Bean
    public Queue AMessage() {
        return new Queue("fanout.A");
    }
 
    @Bean
    public Queue BMessage() {
        return new Queue("fanout.B");
    }
 
    @Bean
    public Queue CMessage() {
        return new Queue("fanout.C");
    }
 
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }
 
    @Bean
    Binding bindingExchangeA(Queue AMessage,FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(AMessage).to(fanoutExchange);
    }
 
    @Bean
    Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(BMessage).to(fanoutExchange);
    }
 
    @Bean
    Binding bindingExchangeC(Queue CMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(CMessage).to(fanoutExchange);
    }
 
}

这里使用了A、B、C三个队列绑定到Fanout交换机上面,发送端的routing_key写任何字符都会被忽略:

public void send() {
        String context = "hi, fanout msg ";
        System.out.println("Sender : " + context);
        this.rabbitTemplate.convertAndSend("fanoutExchange","", context);
}

结果说明,绑定到fanout交换机上面的队列都收到了消息。

参考文章:

https://blog.csdn.net/xiaokang123456kao/article/details/72782138
https://blog.csdn.net/ztx114/article/details/78410727

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值