RabbitMQ-要点简介

Windows下安装RabbitMQ

RabbitMQ是流行的开源消息队列系统,用erlang语言开发,RabbitMQ是AMQP(高级消息队列协议)的标准实现。 要安装RabbitMQ,首先要安装Erlang环境。

环境

Windows10

 

 

安装Erlang环境

Erlang

下载地址:http://www.erlang.org/download/

  1. 直接双击.exe文件执行即可。

 

2.安装完成后,创建环境变量。

打开:控制面板→系统→高级系统设置→环境变量

 

 

3.安装完成后创建一个名叫ERLANG_HOME的环境变量,其指向erlang的安装目录;

 

 

  1. 将%ERLANG_HOME%\bin加入到Path中。

 

 

 

 

  1. 配置完成后,打开命令提示符cmd,输入erl。

    若出现erlang版本显示表示erlang语言环境安装成功。

 

 

安装RabbitMQ

下载地址:https://www.rabbitmq.com/install-windows.html

  1. 直接双击.exe文件执行即可。

 

 

  1. 安装好后,配置环境变量 RABBITMQ_SERVER

 

  1. 将%RABBITMQ_SERVER%\sbin加入到Path中

 

 

安装RabbitMQ-Plugins

RabbitMQ-Plugins相当于是一个管理界面,方便我们在浏览器界面查看RabbitMQ各个消息队列以及exchange的工作情况。

  1. 打开命令行cd进入rabbitmq的sbin目录(或者进入sbin文件下,直接敲cmd,回车)

 

 

 

  1. 输入命令:

    1
    rabbitmq-plugins enable rabbitmq_management

      

    稍等会会发现出现plugins安装成功的提示。

     

 

但如果你在安装插件的过程中出现错误,可以重新启动rabbitmq-service。

若Rabbitmq-service stop在sbin目录下无法运行,错误代码5。

说明权限不够,系统登录的是普通用户,需要管理员的权限执行该命令。

用管理员打开cmd

  1. 先输入rabbitmq-service stop

  2. 接着输入rabbitmq-service remove

  3. 然后输入rabbitmq-service install

  4. 再输入rabbitmq-service start

  5. 最后重新输入rabbitmq-plugins enable rabbitmq_management

 

 

 

管理界面

1.插件安装完之后,在浏览器输入http://localhost:15672进行验证。

 

 

 

 

2.在界面,输入用户名:guest,密码:guest(默认)就可以进入管理界面

 

 

 

几种常用命令

  • 健康检查: rabbitmqctl status

  • 启动监控管理器:rabbitmq-plugins enable rabbitmq_management

  • 关闭监控:rabbitmq-plugins disable rabbitmq_management

  • 停止服务:rabbitmq-service stop

  • 启动服务:rabbitmq-service start

  • 重启命令:net stop RabbitMQ && net start

  • 帮助命令:rabbitmqctl help

  • rabbitmqctl list_queues查看所有队列

  • rabbitmqctl reset清除所有队列

  • rabbitmqctl list_exchanges查看所有交换器

  • rabbitmqctl add_user username password添加用户

  • rabbitmqctl set_user_tags username administrator分配角色

  • rabbitmqctl list_bindings 查看交换器和队列的绑定关系


-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

1、什么是RabbitMQ?
RabbitMQ是由Erlang语言编写的实现了高级消息队列协议(AMQP)的开源消息代理软件(也可称为 面向消息的中间件)。支持Windows、Linux/Unix、MAC OS X操作系统和包括JAVA在内的多种编程语言。

AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受 客户端/中间件 不同产品,不同的开发语言等条件的限制。

2、RabbitMQ中的重要概念

 

 

(1)Broker:经纪人。提供一种传输服务,维护一条从生产者到消费者的传输线路,保证消息数据能按照指定的方式传输。粗略的可以将图中的RabbitMQ Server当作Broker。

(2)Exchange:消息交换机。指定消息按照什么规则路由到哪个队列Queue。

(3)Queue:消息队列。消息的载体,每条消息都会被投送到一个或多个队列中。

(4)Binding:绑定。作用就是将Exchange和Queue按照某种路由规则绑定起来。

(5)RoutingKey:路由关键字。Exchange根据RoutingKey进行消息投递。

(6)Vhost:虚拟主机。一个Broker可以有多个虚拟主机,用作不同用户的权限分离。一个虚拟主机持有一组Exchange、Queue和Binding。

(7)Producer:消息生产者。主要将消息投递到对应的Exchange上面。一般是独立的程序。

(8)Consumer:消息消费者。消息的接收者,一般是独立的程序。

(9)Channel:消息通道,也称信道。在客户端的每个连接里可以建立多个Channel,每个Channel代表一个会话任务。

 

3、RabbitMQ的使用流程
AMQP模型中,消息在producer中产生,发送到MQ的exchange上,exchange根据配置的路由方式投递到相应的Queue上,Queue又将消息发送给已经在此Queue上注册的consumer,消息从queue到consumer有push和pull两种方式。

消息队列的使用过程大概如下:

(1)客户端连接到消息队列服务器,打开一个channel。

(2)客户端声明一个exchange,并设置相关属性。

(3)客户端声明一个queue,并设置相关属性。

(4)客户端使用routing key,在exchange和queue之间建立好Binding关系。

(5)生产者客户端投递消息到exchange。

(6)exchange接收到消息后,就根据消息的RoutingKey和已经设置的binding,进行消息路由(投递),将消息投递到一个或多个队列里。

(7)消费者客户端从对应的队列中获取并处理消息。

4、RabbitMQ的优缺点
优点:

(1)由Erlang语言开发,支持大量协议:AMQP、XMPP、SMTP、STOMP。

(2)支持消息的持久化、负载均衡和集群,且集群易扩展。

(3)具有一个Web监控界面,易于管理。

(4)安装部署简单,上手容易,功能丰富,强大的社区支持。

(5)支持消息确认机制、灵活的消息分发机制。

缺点:

(1)由于牺牲了部分性能来换取稳定性,比如消息的持久化功能,使得RabbitMQ在大吞吐量性能方面不及Kafka和ZeroMQ。

(2)由于支持多种协议,使RabbitMQ非常重量级,比较适合企业级开发。

 

因此当需要一个稳定的、高可靠性的、功能强大且易于管理的消息队列可以选择RabbitMQ。如果对消息吞吐量需求较大,且不在乎消息偶尔丢失的情况可以使用Kafka。

5、Exchange类型
5.1、Direct Exchange
(1)名称:直接交换器类型

(2)默认的预先定义exchange名字:空字符串或者amq.direct

(3)作用描述:根据Binding指定的Routing Key,将符合Key的消息发送到Binding的Queue。可以构建点对点消息传输模型。

 

 

如图中RoutingKey分别是error、info、warning,其中error被Binding(绑定)到queue1和queue2上,info和warning被Binding到queue2上。当消息的RoutingKey是error,这条消息将被投递到queue1和queue2中(相当于消息被复制成两个分别投放到两个queue中),然后分别被Consumer1和Consumer2处理。如果消息的RoutingKey是info或者warning,这条消息只会被投递到queue2中,然后被Consumer2处理。如果消息的RoutingKey是其他的字符串,这条消息则会被丢弃。

5.2、Fanout Exchange
(1)名称:广播式交换器类型

(2)默认的预先定义exchange名字:amq.fanout

(3)作用描述:将同一个message发送到所有同该Exchange 绑定的queue。不论RoutingKey是什么,这条消息都会被投递到所有与此Exchange绑定的queue中。

广播式交换器类型的工作方式:不使用任何参数将queue和Exchange进行Binding,发布者publisher向Exchange发送一条消息(注意:直接交换器类型中的producer变成了publisher,其中隐含了两种交换器的区别),然后这条消息被无条件的投递给所有和这个Exchange绑定的queue中。

如图中,没有RoutingKey的限制,只要消息到达Exchange,都会被投递到queue1和queue2中,然后被对应的Consumer处理。

5.3、Topic Exchange
(1)名称:主题交换器类型

(2)默认的预先定义exchange名字:amq.topic

(3)作用描述:根据Binding指定的RoutingKey,Exchange对key进行模式匹配后投递到相应的Queue,模式匹配时符号“#”匹配一个或多个词,符号“*”匹配正好一个词,而且单词与单词之间必须要用“.”符号进行分隔。此模式可以用来支持经典的发布/订阅消息传输模型-使用主题名字空间作为消息寻址模式,将消息传递给那些部分或者全部匹配主题模式的queue。

如图中,假如消息的RoutingKey是American.action.13,这条消息将被投递到Q1和Q2中。假如RoutingKey是American.action.13.test(注意:此处是四个词),这条消息将会被丢弃,因为没有routingkey与之匹配。假如RoutingKey是Chinese.action.13,这条消息将被投递到Q2和Q3中。假如RoutingKey是Chinese.action.13.test,这条消息只会被投递到Q3中,#可以匹配一个或者多个单词,而*只能匹配一个词。

5.4、Headers Exchange
(1)名称:标题交换器类型

(2)默认的预先定义exchange名字:amq.match和amq.headers

(3)作用描述:同direct exchange类似,不同之处是不再使用Routing Key路由,而是使用headers(Message attributes)进行匹配路由到指定Queue。

Headers类型的exchange使用的比较少,它也是忽略routingKey的一种路由方式。是使用Headers来匹配的。Headers是一个键值对,可以定义成HashTable。发送者在发送的时候定义一些键值对,接收者也可以再绑定时候传入一些键值对,两者匹配的话,则对应的队列就可以收到消息。匹配有两种方式all和any。这两种方式是在接收端必须要用键值"x-mactch"来定义。all代表定义的多个键值对都要满足,而any则代码只要满足一个就可以了。fanout,direct,topic exchange的routingKey都需要要字符串形式的,而headers exchange则没有这个要求,因为键值对的值可以是任何类型。
————————————————

RabbitMQ学习(二):Java使用RabbitMQ要点知识

1、maven依赖

<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.3</version>
</dependency>

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

2、RabbitMQ重要方法介绍(基本常用的)
2.1、创建连接
// 创建连接工厂
ConnectionFactory cf = new ConnectionFactory();
// 设置rabbitmq服务器IP地址
cf.setHost("*.*.*.*");
// 设置rabbitmq服务器用户名
cf.setUsername("***");
// 设置rabbitmq服务器密码
cf.setPassword("***");
// 指定端口,默认5672
cf.setPort(AMQP.PROTOCOL.PORT);
// 获取一个新的连接
connection = cf.newConnection();
// 创建一个通道
channel = connection.createChannel();

//关闭管道和连接
channel.close();
connection.close();


2.2、声明队列
/**
* 申明一个队列,如果这个队列不存在,将会被创建
* @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("testQueue", true, false, false, null);

此方法一般由Producer调用创建消息队列。如果由Consumer创建队列,有可能Producer发布消息的时候Queue还没有被创建好,会造成消息丢失的情况。

 

2.3、声明Exchange
/**
* 声明一个 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);

2.4、将queue和Exchange进行绑定(Binding)
/**
* 将队列绑定到Exchange,不需要额外的参数。
* @param queue 队列名称
* @param exchange 交换机名称
* @param routingKey 路由关键字
* @return Queue.BindOk:如果成功创建绑定,则返回绑定确认方法。
* @throws java.io.IOException if an error is encountered
*/
channel.queueBind("testQueue", "leitao", "testRoutingKey");

2.5、发布消息
/**
* 发布一条不用持久化的消息,且设置两个监听。
* @param exchange 消息交换机名称,空字符串将使用直接交换器模式,发送到默认的Exchange=amq.direct。此状态下,RoutingKey默认和Queue名称相同
* @param routingKey 路由关键字
* @param mandatory 监听是否有符合的队列
* @param immediate 监听符合的队列上是有至少一个Consumer
* @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,false,MessageProperties.TEXT_PLAIN,SerializationUtils.serialize(object));
当exchange的值为空字符串或者是amq.direct时,此时的交换器类型默认是direct类型,可以不用单独声明Exchange,也不用单独进行Binding,系统默认将queue名称作为RoutingKey进行了绑定。

 

两个传入参数的含义

mandatory

当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返回给生产者(Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。

immediate

当immediate标志位设置为true时,如果exchange在将消息路由到queue(s)时发现对于的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或者多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。

概括来说,mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。

注意:在RabbitMQ3.0以后的版本里,去掉了immediate参数的支持,发送带immediate=true标记的publish会返回如下错误:

com.rabbitmq.client.AlreadyClosedException: connection is already closed due to connection error;protocol method: #method<connection.close>(reply-code=540, reply-text=NOT_IMPLEMENTED - immediate=true, class-id=60, method-id=40)。

为什么取消支持:immediate标记会影响镜像队列性能,增加代码复杂性,并建议采用“TTL”和“DLX”等方式替代。

 

2.6、接收消息

/**
* 设置消费批量投递数目,一次性投递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);


2.7、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;

3、Producer消息确认机制
3.1、什么是生产者消息确认机制?
没有消息确认模式时,生产者不知道消息是不是已经到达了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 。

3.2、开启confirm模式
如上所说生产者通过调用Channel.confirmSelect()方法将Channel信道设置成confirm模式。

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

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

 

(1)重点方法

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

(2 ) 部分使用代码如下:

//注意:返回的时候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++;
}


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

 

(1)重点代码

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

(2 )部分代码如下:

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


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

/**
* 生产者发送消息到exchange成功的回调方法
*/
void handleAck(long deliveryTag, boolean multiple) throws IOException;
/**
* 生产者发送消息到服务器broker失败的回调方法,服务器丢失了此消息。
* 注意,丢失的消息仍然可以传递给消费者,但broker不能保证这一点。
*/
void handleNack(long deliveryTag, boolean multiple) throws IOException;
其中deliveryTag是Broker给每条消息指定的唯一ID(从1开始);multiple表示是否接收所有的应答消息,比如multiple=true时,发送100条消息成功过后,我们并不会收到100次handleAck方法调用。

 

(1)重要方法

//注册消息确认监听器
channel.addConfirmListener(new MyConfirmListener());
(2)部分使用代码如下:

//注意:返回的时候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));
}
}


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

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

 

(1)重要方法

/**
*1. 开始一个非局部、非排他性消费, 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;


/**
*2
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;

/**3
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;

/**4
相比channel.basicNack,除了没有multiple批量确认机制之外,其他语义完全一样。
如果channel.basicReject(8, true);表示deliveryTag=8的消息处理失败且将该消息重新放回队列。
如果channel.basicReject(8, false);表示deliveryTag=8的消息处理失败且将该消息直接丢弃。
*/
void basicReject(long deliveryTag, boolean requeue) throws IOException;


(2)部分使用代码如下:


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


5、Demo项目整体代码
此demo就是向RabbitMQ服务器上面发送20个消息,消息体是map,里面装的是tagId=数字。然后注册了两个消费者,分别处理奇数和偶数。

5.1、连接工具类

/**
* 连接工具类
*/
public class ConnectionUtil {

Channel channel;
Connection connection;
String queueName;

public ConnectionUtil(String queueName) throws IOException {
this.queueName = queueName;
// 创建连接工厂
ConnectionFactory cf = new ConnectionFactory();
// 设置rabbitmq服务器IP地址
cf.setHost("*.16.0.*");
// 设置rabbitmq服务器用户名
cf.setUsername("*");
// 设置rabbitmq服务器密码
cf.setPassword("*");
cf.setPort(AMQP.PROTOCOL.PORT);
// 获取一个新的连接
connection = cf.newConnection();
// 创建一个通道
channel = connection.createChannel();
/**
*申明一个队列,如果这个队列不存在,将会被创建
* @param queue 队列名称
* @param durable 持久性:true队列会再重启过后存在,但是其中的消息不会存在。
* @param exclusive 是否只能由创建者使用
* @param autoDelete 是否自动删除(没有连接自动删除)
* @param arguments 队列的其他属性(构造参数)
* @return 宣告队列的声明确认方法已成功声明。
* @throws java.io.IOException if an error is encountered
*/
channel.queueDeclare(queueName, true, false, false, null);
}

public void close() throws IOException{
channel.close();
connection.close();
}
}

 

 

5.2、具体生产者

/**
* 消息生产者
*/
public class MessageProducer {

private ConnectionUtil connectionUtil;

public MessageProducer(ConnectionUtil connectionUtil){
this.connectionUtil=connectionUtil;
}
/**
* 发送消息到队列中
*/
public void sendMessage(Serializable object) throws IOException{
/**
* Publish a message
* @param exchange 消息交换机名称,空字符串将使用直接交换器模式,发送到默认的Exchange=amq.direct
* @param routingKey 路由关键字
* @param mandatory 监听是否有符合的队列
* @param BasicProperties 设置消息持久化:MessageProperties.PERSISTENT_TEXT_PLAIN是持久化;MessageProperties.TEXT_PLAIN是非持久化
* @param body 消息对象
* @throws java.io.IOException if an error is encountered
*/
connectionUtil.channel.basicPublish("", connectionUtil.queueName, true, MessageProperties.TEXT_PLAIN, SerializationUtils.serialize(object));
System.out.println("MessageProducer发送了一条消息:"+object);
}
}

 

5.3、公共消费者父类

/**
* 消息消费者基础类
*/
public class MessageConsumer implements Consumer {
//消费者标签,注册成功时由rabbitmq服务器自动生成
protected String consumerTag;

protected ConnectionUtil connectionUtil;

public MessageConsumer(ConnectionUtil connectionUtil){
this.connectionUtil=connectionUtil;
}

public void basicConsume(){
try {
/**
* 设置消费投递数目,一次性投递10条消息。当消费者未确认消息达到10条时,rabbitMQ将不会向此消费者投递消息,直到未确认数小于10条再投递
* @param prefetchCount 投递数目
* @param global 是否针对整个Channel。true表示此投递数是给Channel设置的,false是给Channel上的Consumer设置的。
* @throws java.io.IOException if an error is encountered
*/
connectionUtil.channel.basicQos(10,false);//表示每个消费者最多10条
connectionUtil.channel.basicQos(15,true);//整个传输管道最多15条,具体分到每个消费者身上又不能大于10条
/**
* 开始一个非局部、非排他性消费, 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
*/
connectionUtil.channel.basicConsume(connectionUtil.queueName, false, this);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 收到消息时的回调函数
*/
public void handleDelivery(String arg0, Envelope arg1, BasicProperties arg2, byte[] arg3) throws IOException {
//子类重写覆盖具体操作
}

/**
* 消费者注册成功回调函数
*/
public void handleConsumeOk(String consumerTag) {
this.consumerTag=consumerTag;
System.out.println("消费者:"+consumerTag+",注册成功!");
}

/**
* 手动取消消费者注册成功回调函数
* 当调用Channel类的void basicCancel(String consumerTag) throws IOException;方法触发此回调函数
*/
public void handleCancelOk(String consumerTag) {
System.out.println(consumerTag+" 手动取消消费者注册成功!");
}

/**
* 当消费者因为其他原因被动取消注册时调用,比如queue被删除了。
*/
public void handleCancel(String consumerTag) throws IOException {
System.out.println("因为外部原因消费者:"+consumerTag+" 取消注册!");
}

/**
* 当通道或基础连接被关闭时调用
*/
public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) {
System.out.println("通道或基础连接被关闭");
}

/**
* Called when a <code><b>basic.recover-ok</b></code> is received
* in reply to a <code><b>basic.recover</b></code>. All messages
* received before this is invoked that haven't been <i>ack</i>'ed will be
* re-delivered. All messages received afterwards won't be.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
*/
public void handleRecoverOk(String consumerTag) {

}
}

 

5.4、具体的消费者

/**
* 专门处理偶数消息的消费者
*/
public class EvenConsumer extends MessageConsumer {

public EvenConsumer(ConnectionUtil connectionUtil) {
super(connectionUtil);
}

@Override
public void handleConsumeOk(String consumerTag) {
this.consumerTag=consumerTag;
System.out.println("EvenConsumer消费者:"+consumerTag+",注册成功!");
}

@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("EvenConsumer接收并处理消息:"+tagId);
//通知服务器此消息已经被处理了
connectionUtil.channel.basicAck(envelope.getDeliveryTag(), false);
}else{
//通知服务器消息处理失败,重新放回队列。false表示处理失败消息不放会队列,直接删除
connectionUtil.channel.basicReject(envelope.getDeliveryTag(), true);
}
}
}

 


/**
* 专门处理奇数消息的消费者
*/
public class OddConsumer extends MessageConsumer {

public OddConsumer(ConnectionUtil connectionUtil) {
super(connectionUtil);
}

@Override
public void handleConsumeOk(String consumerTag) {
this.consumerTag=consumerTag;
System.out.println("OddConsumer消费者:"+consumerTag+",注册成功!");
}

@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("OddConsumer接收并处理消息:"+tagId);
//通知服务器此消息已经被处理了
connectionUtil.channel.basicAck(envelope.getDeliveryTag(), false);
}else{
//通知服务器消息处理失败,重新放回队列。false表示处理失败消息不放会队列,直接删除
connectionUtil.channel.basicReject(envelope.getDeliveryTag(), true);
}
}
}


5.5、监听器

/**
*producer发送确认事件。
*/
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方法,消息将返给发送者 。
* 由于3.0版本过后取消了支持immediate,此处不做过多的解释。
*/
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));
}
}

5.6、客户端

public class Client {

public static void main(String[] args) {
new Client();
}

public Client(){
try {
//发消息
publishMessage();
//注册消费者
addConsumer();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void publishMessage() throws IOException, InterruptedException{
ConnectionUtil connectionUtil=new ConnectionUtil("testqueue");
MessageProducer producer=new MessageProducer(connectionUtil);
connectionUtil.channel.confirmSelect();
//注意:返回的时候Return在前,Confirm在后
connectionUtil.channel.addConfirmListener(new MyConfirmListener());
connectionUtil.channel.addReturnListener(new MyReturnListener());
int i=1;
while (i<=10) {
HashMap<String, Object> map=new HashMap<String, Object>();
map.put("tagId", i);
producer.sendMessage(map);
i++;
}
}

public void addConsumer() throws IOException{
ConnectionUtil connectionUtil=new ConnectionUtil("testqueue");
OddConsumer odd=new OddConsumer(connectionUtil);
odd.basicConsume();
EvenConsumer even=new EvenConsumer(connectionUtil);
even.basicConsume();
}

}

 

 

5.7、测试结果
MessageProducer发送了一条消息:{tagId=1}
MessageProducer发送了一条消息:{tagId=2}
MessageProducer发送了一条消息:{tagId=3}
Exchange接收消息:1(deliveryTag)成功!multiple=false
Exchange接收消息:2(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=4}
Exchange接收消息:3(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=5}
Exchange接收消息:4(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=6}
Exchange接收消息:5(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=7}
Exchange接收消息:6(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=8}
Exchange接收消息:7(deliveryTag)成功!multiple=false
Exchange接收消息:8(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=9}
Exchange接收消息:9(deliveryTag)成功!multiple=false
MessageProducer发送了一条消息:{tagId=10}
Exchange接收消息:10(deliveryTag)成功!multiple=false
OddConsumer消费者:amq.ctag-z8s8LaSgYvo02jktCZrCYA,注册成功!
OddConsumer接收并处理消息:1
OddConsumer接收并处理消息:3
OddConsumer接收并处理消息:5
OddConsumer接收并处理消息:7
OddConsumer接收并处理消息:9
EvenConsumer消费者:amq.ctag-LpN6Q5VvNY3wCof2lXqS4A,注册成功!
EvenConsumer接收并处理消息:4
EvenConsumer接收并处理消息:8
EvenConsumer接收并处理消息:2
EvenConsumer接收并处理消息:10
EvenConsumer接收并处理消息:6


6、Demo完整源码下载地址

————————————————

RabbitMQ学习(三):Spring整合RabbitMQ

 

1、maven依赖
<!-- rabbitMQ依赖 -->
<!-- spring相关的其他依赖请参考源码,此处不做过多描述 -->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>

2 、 spring-rabbit 配置
2.1、配置rabbitMQ连接
(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"
/>


(2)采用普通方式配置连接


<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>


2.2、配置RabbitAdmin
<rabbit:admin connection-factory="connectionFactory" />
2.3、定义Queue
<rabbit:queue name="queueTest" durable="true" auto-delete="false"
exclusive="false" />
常用参数解释:

durable:是否持久化

auto-delete:是否当没有连接时自动删除

exclusive:是否只能由创建者使用

 

2.4、定义Exchange,并绑定Queue
(1)定义direct类型


<rabbit:direct-exchange name="exchangeTest"
durable="true" auto-delete="false">
<rabbit:bindings>
<!-- 此处没有指定RoutingKey -->
<rabbit:binding queue="queueTest" key=""></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>


(2)定义topic类型


<rabbit:topic-exchange name="topicexchangetest" durable="true" auto-delete="false">
<rabbit:bindings>
<rabbit:binding queue="" key=""></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>


(3)定义fanout类型

<rabbit:fanout-exchange name=""></rabbit:topic-exchange>

(4)定义headers类型

<rabbit:headers-exchange name=""></rabbit:topic-exchange>

常用参数解释:

durable:是否持久化

auto-delete:是否当没有连接时自动删除

注意:在此例中,如果不把Exchange和queue进行绑定,发送消息的时候ConfirmCallback依然正常执行,因为消息到达了Exchange。但是ReturnCallback就会执行回调方法,传回错误信息:NO_ROUTE。Exchange没有找到指定的Queue,丢弃这条消息并把消息返回给生产者。回调顺序是ReturnCallback在前,ConfirmCallback在后。

 

2.5、定义rabbit template
(1)普通方式配置rabbitTemplate


<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate" id="rabbitTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="exchange" value="exchangeTest" />

<!-- 启动AMQP协议层面事务机制来解决发送确认机制,但是采用事务机制实现会降低RabbitMQ的消息吞吐量。
RabbitMQ团队为我们拿出了更好的方案,即采用发送方确认模式,事务机制和confirmCallback只能二选一 -->
<!-- <property name="channelTransacted" value="true" /> -->

<!-- mandatory 监听是否有符合的队列 -->
<property name="mandatory" value="true"/>

<!-- 设置发送确认回执监听方法 -->
<property name="confirmCallback" ref="confirmcallback" />
<!-- 设置结果返回监听方法 -->
<property name="returnCallback" ref="MyReturnCallback"/>
<!-- 设置消息转换 -->
<property name="messageConverter" ref="JsonMessageConverter" />
</bean>


(2)<rabbit/>方式配置rabbitTemplate和相关监听器


<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
exchange="exchangeTest"
mandatory="true"
confirm-callback="confirmcallback"
return-callback="MyReturnCallback"
encoding="UTF-8"
message-converter="JsonMessageConverter"
>
</rabbit:template>

常用参数解释:

mandatory:监听是否有符合的队列

confirm-callback:消息确认回调类

return-callback:mandatory监听结果回调类

encoding:编码

message-converter:消息转化类

 

(1)其他相关配置


<bean id="JsonMessageConverter" class="org.springframework.amqp.support.converter.JsonMessageConverter">
<!-- if necessary, override the DefaultClassMapper -->
<property name="classMapper" ref="customClassmapper" />
</bean>


<bean id="customClassmapper" class="pruducer.com.taikang.tkh.message.common.CustomClassMapper"/>


<bean id="confirmcallback" class="pruducer.com.taikang.tkh.message.common.ConfirmReturnBack"/>


<bean id="MyReturnCallback" class="pruducer.com.taikang.tkh.message.common.MyReturnCallback"/>


2.6配置Consumer

<rabbit:listener-container
prefetch="10"
connection-factory="connectionFactory"
acknowledge="manual">
<rabbit:listener queues="queueTest" ref="messageReceiver" />
</rabbit:listener-container>

<!-- 自定义消息接收者 -->
<bean id="messageReceiver" class="consumer.com.taikang.tkh.message.listener.MessageConsumer"></bean>

常用参数解释:

prefetch:消息预取数目为每次接收10条。

acknowledge:acknowledge="manual":意为表示该消费者的ack方式为手动 ;acknowledge="auto"表示自动。

 

3、spring-rabbit常用方法介绍
3.1、消息发送
/**
* 发送消息到默认的交换机和队列(不带有自定义参数)
* @param messageObject 消息对象
* @return boolean 发送标记
*/
RabbitTemplate.convertAndSend(messageObject);

/**
* 发送消息到默认的交换机和队列
* @param messageObject 消息对象
* @param messageObject 自定义参数,在监听器ConfirmCallback中可以取到。
* @return boolean 发送标记
*/
RabbitTemplate.correlationConvertAndSend(messageObject,correlationdata);

/**
* 发送消息到指定的队列
* @param queue 队列名称
* @param messageObject 消息对象
* @param messageObject 自定义参数,在监听器ConfirmCallback中可以取到。
* @return boolean 发送标记
*/
RabbitTemplate.convertAndSend(queue, messageObject,correlationdata);

/**
* 发送消息到指定的交换机和队列
* @param exchange 交换机名称
* @param queue 队列名称
* @param messageObject 自定义参数,在监听器ConfirmCallback中可以取到。
* @return boolean 发送标记
*/
RabbitTemplate.convertAndSend(exchange,queue,messageObject,correlationdata);

/**
* 发送消息到默认的交换机和队列(不带有自定义参数)
Send方法还有很多,此处只列举一种
* @param Message AMQP封装的消息对象
* @return void
*/
RabbitTemplate.send(Message message);


注意 : 凡是带有 c onvert 的系统都会自动把消息转换为 AMQP的Message对象;
没有convert的需要自己将发送的对象转换为Message对象。

 

3.2、Confirm监听

public class ConfirmReturnBack implements ConfirmCallback{
/**
* Confirmation callback.
* @param correlationData 回调的相关数据.
* @param ack true for ack, false for nack
* @param cause 专门给NACK准备的一个可选的原因,其他情况为null。
*/
public void confirm(CorrelationData correlationdata, boolean ack, String cause) {
System.out.println("Exchange接收是否成功(ack): " + ack + "。 返回的用户参数(correlationData): " + correlationdata + "。NACK原因(cause) : " + cause);
}
}
注意:CorrelationData 是在发送消息时传入回调方法的参数,可以用于区分消息对象。 CorrelationData对象中只有一个属性 String id。通过这个参数,我们可以区分当前是发送哪一条消息时的回调,并通过ack参数来进行失败重发功能。

3.3、Return监听

/**
* 实现此方法在basicpublish失败时回调
* 相当于 ReturnListener的功能。
* 在发布消息时设置mandatory等于true,
* 监听消息是否有相匹配的队列,没有时ReturnCallback将执行returnedMessage方法,消息将返给发送者
*/
public class MyReturnCallback implements ReturnCallback {

public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
try {
System.out.println("消息发送进指定队列失败:失败原因(+replyText):"+replyText
+";错误代码(replyCode):"+replyCode
+";消息对象:"+new String(message.getBody(),"UTF-8")
+"exchange:"+exchange
+"routingKey:"+routingKey);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}

 

3.4、消息处理

/**
* 消息到达消费者监听类
*/
public class MessageConsumer implements ChannelAwareMessageListener {
/**
* 处理收到的rabbit消息的回调方法。
* @param message AMQP封装消息对象
* @param channel 信道对象,可以进行确认回复
* @throws Exception Any.
*/
public void onMessage(Message message, Channel channel) throws Exception {
try {
String srt2=new String(message.getBody(),"UTF-8");
System.out.println("消费者收到消息:"+srt2);
//成功应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
//失败应答
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}

注意:ChannelAwareMessageListener和MessageListener两个接口都实现对消息到达消费者时的监听,只不过MessageListener的onMessage方法没有Channel参数,不能实现消息手动应答功能。

-------------------------------------------------------------------------------------------------------------------------

SpringBoot整合RabbitMQ实现六种工作模式

RabbitMQ主要有六种工作模式,本文整合SpringBoot分别介绍工作模式的实现。

 

 

前提概念

 

 

生产者

 

 

消息生产者或者发送者,使用P表示:

 

 

队列

 

 

消息从生产端发送到消费端,一定要通过队列转发,使用queue_name表示:

 

 

消费者

 

 

消费的消费者或者接收者,使用C表示,如果有多个消费者也可以用C1C2表示:

 

 

SpringBoot整合RabbitMQ基本配置

 

 

  1. 添加maven依赖

 

 

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

 

 

  1. 添加application.yml 配置

 

 

spring:  rabbitmq:    host: 192.168.3.19    port: 5672    username: admin    password: 123456

 

 

  1. 消息生产

 

 

生产端发送消息,调用RabbitTemplate发送消息,比如:

 

 

@Autowiredprivate RabbitTemplate rabbitTemplate;public String send() {  rabbitTemplate.convertAndSend("routingKey","send message");}

 

 

  1. 消费消息

 

 

消费消息使用队列监听注解@RabbitListener,添加队列名称就能消费发送到队列上的消息了:

 

 

@RabbitListener(queuesToDeclare = @Queue("queue_name"))public void consume(String message) {  // 接收消息}

 

 

1. 简单(simple)模式

 

 

最简单的消息发送

 

 

特点

 

 

  • 生产者是消费者是一一对应,也叫做点对点模式,生产者发送消息经过队列直接发送给消费者。

  • 生产者和消费者在发送和接收消息时,只需要指定队列名称,而不需要指定Exchange交换机。

 

 

代码示例

 

 

生产消息:

 

 

@GetMapping("/simple-send")public String simpleSend() {  rabbitTemplate.convertAndSend("simple","this is news");  return "ok";}

 

 

消费消息

 

 

@RabbitListener(queuesToDeclare = @Queue("simple"))public void consume(String message) {  System.out.println(message);}

 

 

输出:

 

 

this is news

 

 

无需创建交换机和绑定队列,只需要匹配发送端和消费端的队列名称就能成功发送消息。

 

 

2. 工作模式

 

 

在多个消费者之间分配任务

 

 

特点

 

 

  • 工作模式简单模式差不多,只需要生产端、消费端、队列。

  • 不同在于一个生产者、一个队列对应多个消费者,也就是一对多的关系。

  • 在多个消费者之间分配消息(竞争消费者模式),类似轮询发送消息,每个消息都只发给一个消费者。

 

 

代码示例

 

 

  • 生产消息:

 

 

@GetMapping("/work-send")public String simpleSend() {  rabbitTemplate.convertAndSend("work","this is news");  return "ok";}

 

 

  • 消费消息:

 

 

@RabbitListener(queuesToDeclare = @Queue("work"))public void consume(String message) {  System.out.println("first:" + message);}@RabbitListener(queuesToDeclare = @Queue("work"))public void consumeSecond(String message) {  System.out.println("second:" + message);}

 

 

创建一个生产者,两个消费者,发送两条消息,两个消费者分别接收到消息,输出:

 

 

first:this is newssecond:this is news

 

 

两个消费者,轮流消费消息。类似nginx负载均衡

 

 

3. 发布订阅模式

 

 

一次向多个消费者发送消息

 

 

特点

 

 

  • 发布订阅类似广播消息,每个消息可以同时发送给订阅该消息的消费者,

  • 上图中的X表示交换机,使用的扇形交换机(fanout),它将发送的消息发送到所有绑定交换机的队列。

 

 

代码示例

 

 

  • 创建队列、交换机以及绑定:

 

 

@Beanpublic FanoutExchange fanoutExchange() {  return new FanoutExchange("PUBLISH_SUBSCRIBE_EXCHANGE");}@Beanpublic Queue psFirstQueue() {  return new Queue("psFirstQueue");}@Beanpublic Queue psSecondQueue() {  return new Queue("psSecondQueue");}@Beanpublic Queue psThirdQueue() {  return new Queue("psThirdQueue");}@Beanpublic Binding routingFirstBinding() {  return BindingBuilder.bind(psFirstQueue()).to(fanoutExchange());}@Beanpublic Binding routingSecondBinding() {  return BindingBuilder.bind(psSecondQueue()).to(fanoutExchange());}@Beanpublic Binding routingThirdBinding() {  return BindingBuilder.bind(psThirdQueue()).to(fanoutExchange());}折叠

 

 

  • 上面定义一个交换机fanoutExchange

  • 分别绑定三个队列psFirstQueuepsSecondQueuepsThirdQueue

  • 队列绑定交换机不需要routingKey,直接绑定即可。

 

 

  • 生产端:

 

 

@GetMapping("/publish-sub-send")public String publishSubSend() {  rabbitTemplate.convertAndSend("PUBLISH_SUBSCRIBE_EXCHANGE", null, "publish/subscribe hello");  return "ok";}

 

 

无需指定routingKey,设置为null

 

 

  • 消费端:

 

 

@RabbitListener(queues = "psFirstQueue")public void pubsubQueueFirst(String message) {  System.out.println("【first】:" + message);}@RabbitListener(queues = "psSecondQueue")public void pubsubQueueSecond(String message) {  System.out.println("【second】:" + message);}@RabbitListener(queues = "psThirdQueue")public void pubsubQueueThird(String message) {  System.out.println("【third】:" + message);}

 

 

  • 输出:

 

 

【first】: publish/subscribe hello【second】: publish/subscribe hello【third】: publish/subscribe hello

 

 

发送一条消息,绑定的队列都能接收到消息。

 

 

4. 路由模式

 

 

根据routingKey有选择性的接收消息

 

 

特点

 

 

  • 每个队列根据不同routingKey绑定交换机

  • 消息发送到交换机后通过routingKey发送给特定的队列,然后传到消费者消费。

  • 交换由扇形交换机(fanout)改成直连交换机(direct)。

 

 

代码示例

 

 

  • 创建队列、交换机以及绑定:

 

 

@Beanpublic Queue routingFirstQueue() {    return new Queue("routingFirstQueue");}@Beanpublic Queue routingSecondQueue() {    return new Queue("routingSecondQueue");}@Beanpublic Queue routingThirdQueue() {    return new Queue("routingThirdQueue");}@Beanpublic DirectExchange routingExchange() {    return new DirectExchange("routingExchange");}@Beanpublic Binding routingFirstBind() {    return BindingBuilder.bind(routingFirstQueue()).to(routingExchange()).with("firstRouting");}@Beanpublic Binding routingSecondBind() {    return BindingBuilder.bind(routingSecondQueue()).to(routingExchange()).with("secondRouting");}@Beanpublic Binding routingThirdBind() {    return BindingBuilder.bind(routingThirdQueue()).to(routingExchange()).with("thirdRouting");}折叠

 

 

  • 创建一个交换机,根据不同的路由规则匹配不同的队列routingExchange,根据不同的routingKey绑定不同的队列:

  • firstRouting路由键绑定routingFirstQueue队列。

  • secondRouting路由键绑定routingSecondQueue队列。

  • thirdRouting路由键绑定routingThirdQueue队列。

 

 

  • 生产消息:

 

 

@GetMapping("/routing-first")public String routingFirst() {    // 使用不同的routingKey 转发到不同的队列    rabbitTemplate.convertAndSend("routingExchange","firstRouting"," first routing message");    rabbitTemplate.convertAndSend("routingExchange","secondRouting"," second routing message");    rabbitTemplate.convertAndSend("routingExchange","thirdRouting"," third routing message");    return "ok";}

 

 

  • 消费消息:

 

 

@RabbitListener(queues = "routingFirstQueue")public void routingFirstListener(String message) {    System.out.println("【routing first】" + message);}@RabbitListener(queues = "routingSecondQueue")public void routingSecondListener(String message) {    System.out.println("【routing second】" + message);}@RabbitListener(queues = "routingThirdQueue")public void routingThirdListener(String message) {    System.out.println("【routing third】" + message);}

 

 

输出:

 

 

【routing first】first routing message【routing second】second routing message【routing third】third routing message

 

 

分析:

 

 

rabbitTemplate.convertAndSend("routingExchange","firstRouting"," first routing message");

 

 

消息从生产者指定firstRouting路由键,找到对应的绑定队列routingFirstQueue,就被routingFirstQueue队列消费了。

 

 

5. 主题模式

 

 

基于某个主题接收消息

 

 

特点

 

 

路由模式发送的消息,是需要指定固定的routingKey,如果想要针对一类路由。
比如:

 

 

  • 只接收以.com结尾的消息。

  • www.开头的消息。

 

 

主题模式就派上场了,路由模式主题模式类似,路由模式是设置特定的routingKey绑定唯一的队列,而主题模式的是使用通配符匹配一个或者多个队列。

 

 

代码示例

 

 

  • 创建交换机和队列:

 

 

@Beanpublic Queue topicFirstQueue() {    return new Queue("topicFirstQueue");}@Beanpublic Queue topicSecondQueue() {    return new Queue("topicSecondQueue");}@Beanpublic Queue topicThirdQueue() {    return new Queue("topicThirdQueue");}@Beanpublic TopicExchange topicExchange() {    return new TopicExchange("topicExchange");}

 

 

  • 使用通配符绑定交换机和交换机:

 

 

@Beanpublic Binding topicFirstBind() {    // .com 为结尾    return BindingBuilder.bind(topicFirstQueue()).to(topicExchange()).with("*.com");}@Beanpublic Binding topicSecondBind() {    // www.为开头    return BindingBuilder.bind(topicSecondQueue()).to(topicExchange()).with("www.#");}

 

 

通配符有两种,*#,

 

 

  • * 表示可以匹配一个

  • # 表示可以匹配多个

 

 

比如:

 

 

  • #.com表示接收多个.com结尾的字段。

    • 例如: taobao.comwww.taobao.comwww.jd.com

  • *.com表示接收一个.com结尾的字段。

    • 例如: taobao.comjd.com

    • 多个字段是无法匹配的,比如www.taobao.comcn.taobao.com

  • www.#可以匹配多个www开头的字段。

    • 例如www.taobaowww.jd

  • www.*可以匹配一个www开头的字段。

    • 例如:www.taobaowww.jd

    • 多个字段是无法匹配的,比如www.taobao.comwww.jd.com

  • 生产消息:

 

 

@GetMapping("/topic-first-send")public String topicFirstSend() {    rabbitTemplate.convertAndSend("topicExchange","www.taobao.com","www.taobao.com");    rabbitTemplate.convertAndSend("topicExchange","taobao.com","taobao.com");    rabbitTemplate.convertAndSend("topicExchange","www.jd","www.jd");    return "topic ok";}

 

 

  • 消费消息:

 

 

@RabbitListener(queues = "topicFirstQueue")public void topicFirstListener(String message) {    System.out.println("【topic first】" + message);}@RabbitListener(queues = "topicSecondQueue")public void topicSecondListener(String message) {    System.out.println("【topic second】" + message);}

 

 

  • 输出:

 

 

【topic second】www.taobao.com【topic first】taobao.com【topic second】www.jd

 

 

www.#可以匹配多个以www.开头的路由键,例如www.taobao.comwww.jd。而*.com只能匹配一个以.com结尾的路由键,例如taobao.com,而无法匹配www.taobao.com

 

 

6. RPC模式

 

 

消息有返回值

 

 

特点

 

 

  • PRC模式和上面的几种模式唯一不同的点在于,该模式可以收到消费端的返回值

  • 生成端接收消费端的返回值。

 

 

代码示例

 

 

  • 消费端添加返回值:

 

 

@RabbitListener(queuesToDeclare =@Queue("rpcQueue"))public String rpcListener(String message) {  System.out.println("【rpc接收消息】" + message);  return "rpc 返回" + message;}

 

 

  • 生产端发送消息:

 

 

@GetMapping("/rpc-send")	public void rpcSend() {		Object receive = rabbitTemplate.convertSendAndReceive("rpcQueue","rpc send message");		System.out.println("【发送消息消息】" + receive);	}

 

 

  • 输出:

 

 

【rpc接收消息】rpc send message【发送端接收消息】rpc 返回rpc send message

 

 

交换机类型

 

 

上面的 订阅发布模式路由模式以及主题模式使用到了不同的交换机,分别是:

 

 

  • 直连交换机 Direct

  • 扇形交换机 Fanout

  • 主题交换器 Topic

 

 

Direct Exchange(直连)

 

 

直连交换机被应用在路由模式下,该交换机需要通过特定的routingKey来绑定队列,交换机只有接收到了匹配的routingKey才会将消息转发到对应的队列中,否则就不会转发消息。

 

 

路由模式使用直连交换机,该模式下根据routingKey绑定特定的队列。

 

 

Fanout Exchange(扇形)

 

 

扇形交换机没有路由键的概念,只需将队列绑定在交换机上,发送到交换机上的消息会转发到交换机所以绑定的队列里面,类似广播,只要打开收音机都能接收到广播消息。扇形交换机应用于发布订阅模式

 

 

Topic Exchange(主题)

 

 

主题模式是将路由键根据一个主题进行分类,和直连模式不同的是,直连模式绑定特定的路由键,而主题模式使用通配符绑定路由键,绑定键有两种:

 

 

  • * 表示可以匹配仅一个

  • # 表示可以匹配零个或多个

 

 

总结

 

 

整合SpringBoot实现RabbitMQ六种工作模式,并详细讲解RabbitMQ六种工作模式:

 

 

  • 简单模式

    • 无需创建交换机,匹配生产端和消费的routingKey即可。

  • 工作模式

    • 多个消费端公平竞争同一个消息。

  • 发布订阅模式

    • 一次向多个消费者发送消息。

  • 路由模式

    • 根据特定的路由键转发消息。

  • 主题模式

    • 根据通配符,匹配路由键转发消息。

  • RPC模式

    • 生产端接收消费端发送的返回值。

-------------------------------------------------------------------------------------------------------------------------

 

RabbitMQ的广播消息模式-基于FanoutExchange

创建队列、交换机和绑定对象
/**
* 广播消息模式的配置类:可以将多个队列绑定到同一个交换机(FanoutExchange)上,
* 注意广播消息模板在绑定队列到交换机上时,是不需要指定路由key的(就算指定了也不生效)。
*
* @author huangqiqin
* @date 2021/04/19 00:15
**/
@Configuration
public class FanoutConfig {

@Autowired
private Environment env;

/**
* 创建第一个队列:广播模式
* @return
*/
@Bean(name = "fanoutQueueOne")
public Queue fanoutQueueOne(){
return new Queue(env.getProperty("mq.fanout.queue.one.name"), true);
}

/**
* 创建第二个队列:广播模式
* @return
*/
@Bean(name = "fanoutQueueTwo")
public Queue fanoutQueueTwo(){
return new Queue(env.getProperty("mq.fanout.queue.two.name"), true);
}

/**
* 创建交换机:广播模式
* @return
*/
@Bean(name = "fanoutExchange")
public FanoutExchange fanoutExchange(){
return new FanoutExchange(env.getProperty("mq.fanout.exchange.name"), true, false);
}

/**
* 创建绑定:将队列fanoutQueueOne绑定到fanoutExchange(广播消息)交换机上
* @return
*/
@Bean(name = "fanoutBindingOne")
public Binding fanoutBindingOne(Queue fanoutQueueOne, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueueOne).to(fanoutExchange);
}

/**
* 创建绑定:将队列fanoutQueueTwo绑定到fanoutExchange(广播消息)交换机上
* @return
*/
@Bean(name = "fanoutBindingTwo")
public Binding fanoutBindingTwo(Queue fanoutQueueTwo, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueueTwo).to(fanoutExchange);
}
}


其中properties文件中配置内容如下:

## 以下为广播消息模式的配置项
# 队列一
mq.fanout.queue.one.name=fanout.queue.one.name
# 队列二
mq.fanout.queue.two.name=fanout.queue.two.name
# 交换机
mq.fanout.exchange.name=fanout.exchange.name

创建广播消息的发送者
/**
* 广播消息模板的发送者
* @author huangqiqin
* @date 2021/04/19 00:29
**/
@Component
public class FanoutPublisher {

@Autowired
private Environment env;

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private ObjectMapper objectMapper;

/**
* 发送消息
* @param obj
*/
public void sendFanoutMessage(Object obj){
try {
if (obj != null){
// 设置消息传输格式(JSON)
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
// 设置广播交换机
rabbitTemplate.setExchange(env.getProperty("mq.fanout.exchange.name"));
// 创建消息实例
Message message = MessageBuilder.withBody(objectMapper.writeValueAsBytes(obj)).build();
// 发送消息
rabbitTemplate.convertAndSend(message);
System.out.println("广播消息已发送:" + obj);
}
} catch (Exception e){
e.printStackTrace();
}
}
}


创建广播消息的消费者
/**
* 广播消息模式的消费者
* @author huangqiqin
* @date 2021/04/19 00:37
**/
@Component
public class FanoutConsumer {

@Autowired
private ObjectMapper objectMapper;

/**
* 监听队列一的消费者
* @param msg
*/
@RabbitListener(queues = "${mq.fanout.queue.one.name}", containerFactory = "singleListenerContainer")
public void consumerFanoutMessageOne(@Payload byte[] msg){
try {
Person person = objectMapper.readValue(msg, Person.class);
System.out.println("==> 接收到队列一的消息:" + person);
} catch (Exception e){
e.printStackTrace();
}
}

/**
* 监听队列二的消费者
* @param msg
*/
@RabbitListener(queues = "${mq.fanout.queue.two.name}", containerFactory = "singleListenerContainer")
public void consumerFanoutMessageTwo(@Payload byte[] msg){
try {
Person person = objectMapper.readValue(msg, Person.class);
System.out.println("==> 接收到队列二的消息:" + person);
} catch (Exception e){
e.printStackTrace();
}
}
}

单元测试-测试发送广播消息
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class FanoutPublisherTest {

@Autowired
private FanoutPublisher publisher;

@Test
public void sendFanoutMessage() {

Person person = new Person(1002, "张三丰", 110);

publisher.sendFanoutMessage(person);
}
}

完整的示例代码:https://gitee.com/huangqiqin/middleware-study/tree/master/rabbitmq

-------------------------------------------------------------------------------------------------------------------------

RabbitMQ的5种核心消息模式

简介

RabbitMQ是最受欢迎的开源消息中间件之一,在全球范围内被广泛应用。RabbitMQ是轻量级且易于部署的,能支持多种消息协议。RabbitMQ可以部署在分布式系统中,以满足大规模、高可用的要求。

相关概念

我们先来了解下RabbitMQ中的相关概念,这里以5种消息模式中的路由模式为例。

 

 

标志中文名英文名描述
P 生产者 Producer 消息的发送者,可以将消息发送到交换机
C 消费者 Consumer 消息的接收者,从队列中获取消息并进行消费
X 交换机 Exchange 接收生产者发送的消息,并根据路由键发送给指定队列
Q 队列 Queue 存储从交换机发来的消息
type 交换机类型 type 不同类型的交换机转发消息方式不同
fanout 发布/订阅模式 fanout 广播消息给所有绑定交换机的队列
direct 路由模式 direct 根据路由键发送消息
topic 通配符模式 topic 根据路由键的匹配规则发送消息

5种消息模式

这5种消息模式是构建基于RabbitMQ的消息应用的基础,一定要牢牢掌握它们。学过RabbitMQ的朋友应该了解过这些消息模式的Java实现,这里我们使用Spring AMQP的形式来实现它们。

简单模式

简单模式是最简单的消息模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。

模式示意图

 

 

Spring AMQP实现

  • 首先需要在pom.xml中添加Spring AMQP的相关依赖;
<!--Spring AMQP依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
复制代码
  • 然后修改application.yml,添加RabbitMQ的相关配置;
spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: /mall
    username: mall
    password: mall
    publisher-confirms: true #消息发送到交换器确认
    publisher-returns: true #消息发送到队列确认
复制代码
  • 添加简单模式相关Java配置,创建一个名为simple.hello的队列、一个生产者和一个消费者;
/**
 * Created by macro on 2020/5/19.
 */
@Configuration
public class SimpleRabbitConfig {

	@Bean
	public Queue hello() {
		return new Queue("simple.hello");
	}

	@Bean
	public SimpleSender simpleSender(){
		return new SimpleSender();
	}

	@Bean
	public SimpleReceiver simpleReceiver(){
		return new SimpleReceiver();
	}

}
复制代码
  • 生产者通过send方法向队列simple.hello中发送消息;
/**
 * Created by macro on 2020/5/19.
 */
public class SimpleSender {

	private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSender.class);

	@Autowired
	private RabbitTemplate template;

	private static final String queueName="simple.hello";

	public void send() {
		String message = "Hello World!";
		this.template.convertAndSend(queueName, message);
		LOGGER.info(" [x] Sent '{}'", message);
	}

}
复制代码
  • 消费者从队列simple.hello中获取消息;
/**
 * Created by macro on 2020/5/19.
 */
@RabbitListener(queues = "simple.hello")
public class SimpleReceiver {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleReceiver.class);

    @RabbitHandler
    public void receive(String in) {
        LOGGER.info(" [x] Received '{}'", in);
    }

}
复制代码
  • 在controller中添加测试接口,调用该接口开始发送消息;
/**
 * Created by macro on 2020/5/19.
 */
@Api(tags = "RabbitController", description = "RabbitMQ功能测试")
@Controller
@RequestMapping("/rabbit")
public class RabbitController {

    @Autowired
    private SimpleSender simpleSender;

    @ApiOperation("简单模式")
    @RequestMapping(value = "/simple", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult simpleTest() {
        for(int i=0;i<10;i++){
            simpleSender.send();
            ThreadUtil.sleep(1000);
        }
        return CommonResult.success(null);
    }
}
复制代码
  • 运行后结果如下,可以发现生产者往队列中发送消息,消费者从队列中获取消息并消费。

 

 

 

 

工作模式

工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、两个消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息。

模式示意图

 

 

Spring AMQP实现

  • 添加工作模式相关Java配置,创建一个名为work.hello的队列、一个生产者和两个消费者;
/**
 * Created by macro on 2020/5/19.
 */
@Configuration
public class WorkRabbitConfig {

    @Bean
    public Queue workQueue() {
        return new Queue("work.hello");
    }

    @Bean
    public WorkReceiver workReceiver1() {
        return new WorkReceiver(1);
    }

    @Bean
    public WorkReceiver workReceiver2() {
        return new WorkReceiver(2);
    }

    @Bean
    public WorkSender workSender() {
        return new WorkSender();
    }

}
复制代码
  • 生产者通过send方法向队列work.hello中发送消息,消息中包含一定数量的.号;
/**
 * Created by macro on 2020/5/19.
 */
public class WorkSender {

    private static final Logger LOGGER = LoggerFactory.getLogger(WorkSender.class);

    @Autowired
    private RabbitTemplate template;

    private static final String queueName = "work.hello";

    public void send(int index) {
        StringBuilder builder = new StringBuilder("Hello");
        int limitIndex = index % 3+1;
        for (int i = 0; i < limitIndex; i++) {
            builder.append('.');
        }
        builder.append(index+1);
        String message = builder.toString();
        template.convertAndSend(queueName, message);
        LOGGER.info(" [x] Sent '{}'", message);
    }

}
复制代码
  • 两个消费者从队列work.hello中获取消息,名称分别为instance 1instance 2,消息中包含.号越多,耗时越长;
/**
 * Created by macro on 2020/5/19.
 */
@RabbitListener(queues = "work.hello")
public class WorkReceiver {

    private static final Logger LOGGER = LoggerFactory.getLogger(WorkReceiver.class);

    private final int instance;

    public WorkReceiver(int i) {
        this.instance = i;
    }

    @RabbitHandler
    public void receive(String in) {
        StopWatch watch = new StopWatch();
        watch.start();
        LOGGER.info("instance {} [x] Received '{}'", this.instance, in);
        doWork(in);
        watch.stop();
        LOGGER.info("instance {} [x] Done in {}s", this.instance, watch.getTotalTimeSeconds());
    }

    private void doWork(String in) {
        for (char ch : in.toCharArray()) {
            if (ch == '.') {
                ThreadUtil.sleep(1000);
            }
        }
    }

}
复制代码
  • 在controller中添加测试接口,调用该接口开始发送消息;
/**
 * Created by macro on 2020/5/19.
 */
@Api(tags = "RabbitController", description = "RabbitMQ功能测试")
@Controller
@RequestMapping("/rabbit")
public class RabbitController {
    
    @Autowired
    private WorkSender workSender;

    @ApiOperation("工作模式")
    @RequestMapping(value = "/work", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult workTest() {
        for(int i=0;i<10;i++){
            workSender.send(i);
            ThreadUtil.sleep(1000);
        }
        return CommonResult.success(null);
    }
}
复制代码
  • 运行后结果如下,可以发现生产者往队列中发送包含不同数量.号的消息,instance 1instance 2消费者互相竞争,分别消费了一部分消息。

 

 

 

 

 

 

发布/订阅模式

发布/订阅模式是指同时向多个消费者发送消息的模式(类似广播的形式),它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。

模式示意图

 

 

Spring AMQP实现

  • 添加发布/订阅模式相关Java配置,创建一个名为exchange.fanout的交换机、一个生产者、两个消费者和两个匿名队列,将两个匿名队列都绑定到交换机;
/**
 * Created by macro on 2020/5/19.
 */
@Configuration
public class FanoutRabbitConfig {

    @Bean
    public FanoutExchange fanout() {
        return new FanoutExchange("exchange.fanout");
    }

    @Bean
    public Queue fanoutQueue1() {
        return new AnonymousQueue();
    }

    @Bean
    public Queue fanoutQueue2() {
        return new AnonymousQueue();
    }

    @Bean
    public Binding fanoutBinding1(FanoutExchange fanout, Queue fanoutQueue1) {
        return BindingBuilder.bind(fanoutQueue1).to(fanout);
    }

    @Bean
    public Binding fanoutBinding2(FanoutExchange fanout, Queue fanoutQueue2) {
        return BindingBuilder.bind(fanoutQueue2).to(fanout);
    }

    @Bean
    public FanoutReceiver fanoutReceiver() {
        return new FanoutReceiver();
    }

    @Bean
    public FanoutSender fanoutSender() {
        return new FanoutSender();
    }

}
复制代码
  • 生产者通过send方法向交换机exchange.fanout中发送消息,消息中包含一定数量的.号;
/**
 * Created by macro on 2020/5/19.
 */
public class FanoutSender {
    private static final Logger LOGGER = LoggerFactory.getLogger(FanoutSender.class);
    @Autowired
    private RabbitTemplate template;

    private static final String exchangeName = "exchange.fanout";

    public void send(int index) {
        StringBuilder builder = new StringBuilder("Hello");
        int limitIndex = index % 3 + 1;
        for (int i = 0; i < limitIndex; i++) {
            builder.append('.');
        }
        builder.append(index + 1);
        String message = builder.toString();
        template.convertAndSend(exchangeName, "", message);
        LOGGER.info(" [x] Sent '{}'", message);
    }

}
复制代码
  • 消费者从绑定的匿名队列中获取消息,消息中包含.号越多,耗时越长,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1instance 2
/**
 * Created by macro on 2020/5/19.
 */
public class FanoutReceiver {

    private static final Logger LOGGER = LoggerFactory.getLogger(FanoutReceiver.class);

    @RabbitListener(queues = "#{fanoutQueue1.name}")
    public void receive1(String in) {
        receive(in, 1);
    }

    @RabbitListener(queues = "#{fanoutQueue2.name}")
    public void receive2(String in) {
        receive(in, 2);
    }

    private void receive(String in, int receiver) {
        StopWatch watch = new StopWatch();
        watch.start();
        LOGGER.info("instance {} [x] Received '{}'", receiver, in);
        doWork(in);
        watch.stop();
        LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds());
    }

    private void doWork(String in) {
        for (char ch : in.toCharArray()) {
            if (ch == '.') {
                ThreadUtil.sleep(1000);
            }
        }
    }

}
复制代码
  • 在controller中添加测试接口,调用该接口开始发送消息;
/**
 * Created by macro on 2020/5/19.
 */
@Api(tags = "RabbitController", description = "RabbitMQ功能测试")
@Controller
@RequestMapping("/rabbit")
public class RabbitController {
    
    @Autowired
    private FanoutSender fanoutSender;

    @ApiOperation("发布/订阅模式")
    @RequestMapping(value = "/fanout", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult fanoutTest() {
        for(int i=0;i<10;i++){
            fanoutSender.send(i);
            ThreadUtil.sleep(1000);
        }
        return CommonResult.success(null);
    }
}
复制代码
  • 运行后结果如下,可以发现生产者往队列中发送包含不同数量.号的消息,instance 1instance 2同时获取并消费了消息。

 

 

 

 

 

 

路由模式

路由模式是可以根据路由键选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键转发到不同队列,队列绑定的消费者接收并消费消息。

模式示意图

 

 

Spring AMQP实现

  • 添加路由模式相关Java配置,创建一个名为exchange.direct的交换机、一个生产者、两个消费者和两个匿名队列,队列通过路由键都绑定到交换机,队列1的路由键为orangeblack队列2的路由键为greenblack
/**
 * Created by macro on 2020/5/19.
 */
@Configuration
public class DirectRabbitConfig {

    @Bean
    public DirectExchange direct() {
        return new DirectExchange("exchange.direct");
    }

    @Bean
    public Queue directQueue1() {
        return new AnonymousQueue();
    }

    @Bean
    public Queue directQueue2() {
        return new AnonymousQueue();
    }

    @Bean
    public Binding directBinding1a(DirectExchange direct, Queue directQueue1) {
        return BindingBuilder.bind(directQueue1).to(direct).with("orange");
    }

    @Bean
    public Binding directBinding1b(DirectExchange direct, Queue directQueue1) {
        return BindingBuilder.bind(directQueue1).to(direct).with("black");
    }

    @Bean
    public Binding directBinding2a(DirectExchange direct, Queue directQueue2) {
        return BindingBuilder.bind(directQueue2).to(direct).with("green");
    }

    @Bean
    public Binding directBinding2b(DirectExchange direct, Queue directQueue2) {
        return BindingBuilder.bind(directQueue2).to(direct).with("black");
    }

    @Bean
    public DirectReceiver receiver() {
        return new DirectReceiver();
    }


    @Bean
    public DirectSender directSender() {
        return new DirectSender();
    }

}
复制代码
  • 生产者通过send方法向交换机exchange.direct中发送消息,发送时使用不同的路由键,根据路由键会被转发到不同的队列;
/**
 * Created by macro on 2020/5/19.
 */
public class DirectSender {

	@Autowired
	private RabbitTemplate template;

	private static final String exchangeName = "exchange.direct";

	private final String[] keys = {"orange", "black", "green"};

	private static final Logger LOGGER = LoggerFactory.getLogger(DirectSender.class);

	public void send(int index) {
		StringBuilder builder = new StringBuilder("Hello to ");
		int limitIndex = index % 3;
		String key = keys[limitIndex];
		builder.append(key).append(' ');
		builder.append(index+1);
		String message = builder.toString();
		template.convertAndSend(exchangeName, key, message);
		LOGGER.info(" [x] Sent '{}'", message);
	}

}
复制代码
  • 消费者从自己绑定的匿名队列中获取消息,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1instance 2
/**
 * Created by macro on 2020/5/19.
 */
public class DirectReceiver {
    private static final Logger LOGGER = LoggerFactory.getLogger(DirectReceiver.class);

    @RabbitListener(queues = "#{directQueue1.name}")
    public void receive1(String in){
        receive(in, 1);
    }

    @RabbitListener(queues = "#{directQueue2.name}")
    public void receive2(String in){
        receive(in, 2);
    }

    private void receive(String in, int receiver){
        StopWatch watch = new StopWatch();
        watch.start();
        LOGGER.info("instance {} [x] Received '{}'", receiver, in);
        doWork(in);
        watch.stop();
        LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds());
    }

    private void doWork(String in){
        for (char ch : in.toCharArray()) {
            if (ch == '.') {
                ThreadUtil.sleep(1000);
            }
        }
    }

}
复制代码
  • 在controller中添加测试接口,调用该接口开始发送消息;
/**
 * Created by macro on 2020/5/19.
 */
@Api(tags = "RabbitController", description = "RabbitMQ功能测试")
@Controller
@RequestMapping("/rabbit")
public class RabbitController {

    @Autowired
    private DirectSender directSender;

    @ApiOperation("路由模式")
    @RequestMapping(value = "/direct", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult directTest() {
        for(int i=0;i<10;i++){
            directSender.send(i);
            ThreadUtil.sleep(1000);
        }
        return CommonResult.success(null);
    }
}
复制代码
  • 运行后结果如下,可以发现生产者往队列中发送包含不同路由键的消息,instance 1获取到了orangeblack消息,instance 2获取到了greenblack消息。

 

 

 

 

 

 

通配符模式

通配符模式是可以根据路由键匹配规则选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键匹配规则绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键匹配规则转发到不同队列,队列绑定的消费者接收并消费消息。

特殊匹配符号

  • *:只能匹配一个单词;
  • #:可以匹配零个或多个单词。

模式示意图

 

 

Spring AMQP实现

  • 添加通配符模式相关Java配置,创建一个名为exchange.topic的交换机、一个生产者、两个消费者和两个匿名队列,匹配*.orange.**.*.rabbit发送到队列1,匹配lazy.#发送到队列2
/**
 * Created by macro on 2020/5/19.
 */
@Configuration
public class TopicRabbitConfig {

    @Bean
    public TopicExchange topic() {
        return new TopicExchange("exchange.topic");
    }

    @Bean
    public Queue topicQueue1() {
        return new AnonymousQueue();
    }

    @Bean
    public Queue topicQueue2() {
        return new AnonymousQueue();
    }

    @Bean
    public Binding topicBinding1a(TopicExchange topic, Queue topicQueue1) {
        return BindingBuilder.bind(topicQueue1).to(topic).with("*.orange.*");
    }

    @Bean
    public Binding topicBinding1b(TopicExchange topic, Queue topicQueue1) {
        return BindingBuilder.bind(topicQueue1).to(topic).with("*.*.rabbit");
    }

    @Bean
    public Binding topicBinding2a(TopicExchange topic, Queue topicQueue2) {
        return BindingBuilder.bind(topicQueue2).to(topic).with("lazy.#");
    }

    @Bean
    public TopicReceiver topicReceiver() {
        return new TopicReceiver();
    }

    @Bean
    public TopicSender topicSender() {
        return new TopicSender();
    }

}
复制代码
  • 生产者通过send方法向交换机exchange.topic中发送消息,消息中包含不同的路由键
/**
 * Created by macro on 2020/5/19.
 */
public class TopicSender {

	@Autowired
	private RabbitTemplate template;

	private static final String exchangeName = "exchange.topic";

	private static final Logger LOGGER = LoggerFactory.getLogger(TopicSender.class);


	private final String[] keys = {"quick.orange.rabbit", "lazy.orange.elephant", "quick.orange.fox",
			"lazy.brown.fox", "lazy.pink.rabbit", "quick.brown.fox"};

	public void send(int index) {
		StringBuilder builder = new StringBuilder("Hello to ");
		int limitIndex = index%keys.length;
		String key = keys[limitIndex];
		builder.append(key).append(' ');
		builder.append(index+1);
		String message = builder.toString();
		template.convertAndSend(exchangeName, key, message);
		LOGGER.info(" [x] Sent '{}'",message);
		System.out.println(" [x] Sent '" + message + "'");
	}

}
复制代码
  • 消费者从自己绑定的匿名队列中获取消息,由于该消费者可以从两个队列中获取并消费消息,可以看做两个消费者,名称分别为instance 1instance 2
/**
 * Created by macro on 2020/5/19.
 */
public class TopicReceiver {

	private static final Logger LOGGER = LoggerFactory.getLogger(TopicReceiver.class);

	@RabbitListener(queues = "#{topicQueue1.name}")
	public void receive1(String in){
		receive(in, 1);
	}

	@RabbitListener(queues = "#{topicQueue2.name}")
	public void receive2(String in){
		receive(in, 2);
	}

	public void receive(String in, int receiver){
		StopWatch watch = new StopWatch();
		watch.start();
		LOGGER.info("instance {} [x] Received '{}'", receiver, in);
		doWork(in);
		watch.stop();
		LOGGER.info("instance {} [x] Done in {}s", receiver, watch.getTotalTimeSeconds());
	}

	private void doWork(String in){
		for (char ch : in.toCharArray()) {
			if (ch == '.') {
				ThreadUtil.sleep(1000);
			}
		}
	}

}
复制代码
  • 在controller中添加测试接口,调用该接口开始发送消息;
/**
 * Created by macro on 2020/5/19.
 */
@Api(tags = "RabbitController", description = "RabbitMQ功能测试")
@Controller
@RequestMapping("/rabbit")
public class RabbitController {

    @Autowired
    private TopicSender topicSender;

    @ApiOperation("通配符模式")
    @RequestMapping(value = "/topic", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult topicTest() {
        for(int i=0;i<10;i++){
            topicSender.send(i);
            ThreadUtil.sleep(1000);
        }
        return CommonResult.success(null);
    }
}
复制代码
  • 运行后结果如下,可以发现生产者往队列中发送包含不同路由键的消息,instance 1instance 2分别获取到了匹配的消息。

 

 

 

 

 

 

参考资料

项目源码地址

github.com/macrozheng/…


-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------

 

posted @ 2022-12-02 15:26  hanease  阅读(171)  评论(0)    收藏  举报