RabbitMQ学习笔记
一、RabbitMQ简介
1.介绍
RabbitMQ是一个由erlang开发的基于AMQP(Advanced Message Queue )协议的开源实现。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面都非常的优秀。是当前最主流的消息中间件之一。
2.AMQP
AMQP,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,同样,消息使用者也不用知道发送者的存在。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
3.系统架构

消息队列的使用过程大概如下:
(1)客户端连接到消息队列服务器,打开一个channel。
(2)客户端声明一个exchange,并设置相关属性。
(3)客户端声明一个queue,并设置相关属性。
(4)客户端使用routing key,在exchange和queue之间建立好绑定关系。
(5) 客户端投递消息到exchange。exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
如图所示:AMQP 里主要要说两个组件:Exchange 和 Queue
绿色的 X 就是 Exchange ,红色的是 Queue ,这两者都在 Server 端,又称作 Broker ,这部分是 RabbitMQ 实现的,而蓝色的则是客户端,通常有 Producer 和 Consumer 两种类型。
4.几个概念
P: 为Producer,数据的发送方。
C:为Consumer,数据的接收方。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
Broker:经纪人。提供一种传输服务,维护一条从生产者到消费者的传输线路,保证消息数据能按照指定的方式传输。粗略的可以将图中的RabbitMQ Server当作Broker。
5、RabbitMQ的优缺点
优点:
(1)由Erlang语言开发,支持大量协议:AMQP、XMPP、SMTP、STOMP。
(2)支持消息的持久化、负载均衡和集群,且集群易扩展。
(3)具有一个Web监控界面,易于管理。
(4)安装部署简单,上手容易,功能丰富,强大的社区支持。
(5)支持消息确认机制、灵活的消息分发机制。
缺点:
(1)由于牺牲了部分性能来换取稳定性,比如消息的持久化功能,使得RabbitMQ在大吞吐量性能方面不及Kafka和ZeroMQ。
(2)由于支持多种协议,使RabbitMQ非常重量级,比较适合企业级开发。
二、安装与配置
1、下载安装
Rabbit MQ 是建立在强大的Erlang OTP平台上,因此安装RabbitMQ之前要先安装Erlang。
erlang:http://www.erlang.org/download.html
rabbitmq:http://www.rabbitmq.com/download.html
本人使用的版本是Erlang9.3、rabbitmq-server-3.7.4
2、配置
1. 安装完以后erlang需要手动设置ERLANG_HOME 的系统变量。
输入:set ERLANG_HOME=C:\Program Files\erl9.3
打开命令行,输入erl,可以 查看erlang版本
2.激活Rabbit MQ's Management Plugin
使用Rabbit MQ 管理插件,可以更好的可视化方式查看Rabbit MQ 服务器实例的状态,你可以在命令行中使用下面的命令激活。
输入:rabbitmq-plugins.bat enable rabbitmq_management
同时,我们也使用rabbitmqctl控制台命令(位于rabbitmq_server-3.7.4\sbin)来创建用户,密码,绑定权限等
3.创建管理用户
输入:rabbitmqctl.bat add_user maoluyang maoluyang

4. 设置管理员
输入:rabbitmqctl.bat set_user_tags maoluyang administrator

5.设置权限
输入:rabbitmqctl.bat set_permissions -p / maoluyang ".*" ".*" ".*"

6. 其他命令
a. 查询用户: rabbitmqctl.bat list_users
b. 查询vhosts: rabbitmqctl.bat list_vhosts
c. 启动RabbitMQ服务: net stop RabbitMQ && net start RabbitMQ
7.错误汇总
一、RabbitMQ安装后无法启动问题
打开rabbitmq-server.bat启动时报:ERROR: node with name "rabbit" already running on "DESKTOP-LNB48U3"错误。解决步骤:
1、找到路径:C:\Users\Administrator\AppData\Roaming\RabbitMQ;
2、删除该路径下的文件(如果删不了,请先关掉RabbitMQ服务);
3、重新打开rabbitmq-server.bat,启动成功。
二、启动出现下图,说明安装成功,但是缺少插件

3.Rabbit MQ管理后台
点击rabbitmq_server-3.7.4\sbin下的rabbitmq-server.bat,启动RabbitMQ服务
使用浏览器打开http://localhost:15672 访问Rabbit Mq的管理控制台,使用刚才创建的账号登陆系统即可。
Rabbit MQ 管理后台,可以更好的可视化方式查看RabbitMQ服务器实例的状态。

4. 创建vhosts
1. 创建vhosts, 在admin页面,点击右侧Virtual Hosts

三、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("***"); // 指定端口,默认15672 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进行了绑定。
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=数字。然后注册了两个消费者,分别处理奇数和偶数。
创建maven工程,pom.xml如下:
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.rabbitmq</groupId> <artifactId>rabbitmqdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <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> </dependencies> </project> |
5.1、连接工具类
|
package com.rebbitmq.util;
import java.io.IOException;
import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil { public Channel channel; public Connection connection; public String queueName;
public ConnectionUtil(String queueName) throws IOException { this.queueName = queueName; // 创建连接工厂 ConnectionFactory cf = new ConnectionFactory(); // 设置rabbitmq服务器IP地址 cf.setHost("192.168.0.26"); // 设置rabbitmq服务器用户名 cf.setUsername("maoluyang"); // 设置rabbitmq服务器密码 cf.setPassword("maoluyang"); 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、具体生产者
|
package com.rebbitmq.producer;
import java.io.IOException; import java.io.Serializable;
import org.apache.commons.lang.SerializationUtils;
import com.rabbitmq.client.MessageProperties; import com.rebbitmq.util.ConnectionUtil; /** * * @details 消息生产者 * @author MLY * @date 2018年4月20日 */ 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、公共消费者父类
|
package com.rebbitmq.consumer;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.ShutdownSignalException; import com.rebbitmq.util.ConnectionUtil; /** * * @details消息消费者基础类 * @author MLY * @date 2018年4月20日 */ 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 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) {
}
@Override public void handleDelivery(String arg0, Envelope arg1, BasicProperties arg2, byte[] arg3) throws IOException { // TODO Auto-generated method stub
} }
|
5.4、具体的消费者
|
package com.rebbitmq.consumer;
import java.io.IOException; import java.util.Map;
import org.apache.commons.lang.SerializationUtils;
import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; import com.rebbitmq.util.ConnectionUtil; /** * * @details专门处理偶数消息的消费者 * @author MLY * @date 2018年4月20日 */ 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); } } }
|
|
package com.rebbitmq.consumer;
import java.io.IOException; import java.util.Map;
import org.apache.commons.lang.SerializationUtils;
import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; import com.rebbitmq.util.ConnectionUtil;
/** * * @details专门处理奇数消息的消费者 * @author MLY * @date 2018年4月20日 */ 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、监听器
|
package com.rebbitmq.listener;
import java.io.IOException;
import com.rabbitmq.client.ConfirmListener; /** * * @details producer发送确认事件。 * @author MLY * @date 2018年4月20日 */ 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丢失了消息"); } }
|
|
package com.rebbitmq.listener;
import java.io.IOException;
import org.apache.commons.lang.SerializationUtils;
import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.ReturnListener;
/** * 实现此接口以通知交付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、客户端
|
package com.rebbitmq.client;
import java.io.IOException; import java.util.HashMap;
import com.rebbitmq.consumer.EvenConsumer; import com.rebbitmq.consumer.OddConsumer; import com.rebbitmq.listener.MyConfirmListener; import com.rebbitmq.listener.MyReturnListener; import com.rebbitmq.producer.MessageProducer; import com.rebbitmq.util.ConnectionUtil;
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} MessageProducer发送了一条消息:{tagId=6} Exchange接收消息:5(deliveryTag)成功!multiple=true Exchange接收消息:6(deliveryTag)成功!multiple=false MessageProducer发送了一条消息:{tagId=7} MessageProducer发送了一条消息:{tagId=8} MessageProducer发送了一条消息:{tagId=9} MessageProducer发送了一条消息:{tagId=10} Exchange接收消息:10(deliveryTag)成功!multiple=true OddConsumer消费者:amq.ctag-w5NOLLb4umvFT7-OlWea4w,注册成功! OddConsumer接收并处理消息:1 OddConsumer接收并处理消息:3 OddConsumer接收并处理消息:5 OddConsumer接收并处理消息:7 OddConsumer接收并处理消息:9 EvenConsumer消费者:amq.ctag--V4xtHLkgsDBKXs3vwnQ5g,注册成功! EvenConsumer接收并处理消息:4 EvenConsumer接收并处理消息:8 EvenConsumer接收并处理消息:2 EvenConsumer接收并处理消息:10 EvenConsumer接收并处理消息:6
|
四、Spring整合RabbitMQ
1.pom.xml
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.rabbitmqspring</groupId> <artifactId>rabbitmqspring</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- spring版本号 --> <spring.version>3.2.8.RELEASE</spring.version> <!-- log4j日志文件管理包版本 --> <slf4j.version>1.6.6</slf4j.version> <log4j.version>1.2.12</log4j.version> <!-- junit版本号 --> <junit.version>4.10</junit.version> </properties>
<dependencies> <!-- 添加Spring依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency>
<!--单元测试依赖 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency>
<!-- 日志文件管理包 --> <!-- log start --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <!-- log end -->
<!--spring单元测试依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency>
<!--rabbitmq依赖 --> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>1.1.3.RELEASE</version> </dependency>
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency>
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.0.1.Final</version> </dependency> </dependencies> <build> <resources> <resource> <directory>src/main/resources</directory> <targetPath>${basedir}/target/classes</targetPath> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> <resource> <directory>src/main/resources</directory> <targetPath>${basedir}/target/resources</targetPath> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.1.1</version> <configuration> <warSourceExcludes>${warExcludes}</warSourceExcludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.4.3</version> <configuration> <testFailureIgnore>true</testFailureIgnore> </configuration> </plugin> <plugin> <inherited>true</inherited> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project> |
2.rabbitMQ.xml
|
<?xml version="1.0" encoding="UTF-8"?> <beans default-lazy-init="false" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!--配置connection-factory,指定连接rabbit server参数 --> <rabbit:connection-factory id="connectionFactory" username="maoluyang" password="maoluyang" host="127.0.0.1" port="5672" />
<!--通过指定下面的admin信息,当前proceducer中的exchange和queue会在rabbitmq服务器上自动生成 --> <rabbit:admin connection-factory="connectionFactory" />
<!-- 标准的建立Queue的参数 --> <rabbit:queue-arguments id="amqpQueueArguments"> <!-- 暂时没有 --> </rabbit:queue-arguments>
<rabbit:queue queue-arguments="amqpQueueArguments" id="amqpTemplateReplyQueue" name="test"/>
<!--定义rabbit template用于数据的接收和发送 --> <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" reply-queue="amqpTemplateReplyQueue"> <rabbit:reply-listener concurrency="2"/> </rabbit:template>
<!--定义queue --> <rabbit:queue name="queueTest" id="amqpTemplateRequestQueue" queue-arguments="amqpQueueArguments"/>
<!-- 消息接收者 --> <bean id="messageReceiver" class="com.mao.rabbitmq.Consumer"></bean>
<!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象--> <rabbit:listener-container connection-factory="connectionFactory"> <rabbit:listener queues="amqpTemplateRequestQueue" ref="messageReceiver"/> </rabbit:listener-container>
</beans> |
3.application-context.xml
|
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<import resource="classpath*:rabbitmq.xml" />
<!-- 扫描指定package下所有带有如@controller,@services,@resource,@ods并把所注释的注册为Spring Beans --> <context:component-scan base-package="com.mao.rabbitmq,com.mao.rabbitmq" />
<!-- 激活annotation功能 --> <context:annotation-config /> <!-- 激活annotation功能 --> <context:spring-configured />
</beans> |
4.log4j.properties
|
log4j.rootLogger=debug,Console,Stdout
#Console log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.logger.java.sql.ResultSet=INFO log4j.logger.org.apache=INFO log4j.logger.java.sql.Connection=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
log4j.appender.Stdout = org.apache.log4j.DailyRollingFileAppender log4j.appender.Stdout.File = E://logs/log.log log4j.appender.Stdout.Append = true log4j.appender.Stdout.Threshold = DEBUG log4j.appender.Stdout.layout = org.apache.log4j.PatternLayout log4j.appender.Stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n |
5.创建消费者和生产者
Proceducer.java
|
package com.mao.rabbitmq; import javax.annotation.Resource;
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.stereotype.Service;
/** * 功能概要:消息产生,提交到队列中去 * */ @Service public class Proceducer {
private Logger logger = LoggerFactory.getLogger(Proceducer.class);
@Resource private AmqpTemplate amqpTemplate;
public void sendMessage(Object message){ //发送消息到消息队列服务器中,并得到回馈内容 Object object=amqpTemplate.convertSendAndReceive("queueTest",message,new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { MessageProperties props = message.getMessageProperties(); //把版本加入消息头中 props.setHeader("header", "1.0.0"); props.setExpiration(String.valueOf(30000)); logger.debug("设置RPC消息的TTL为{}", 30000); return message; } }); System.out.println(object); } } |
Consumer.java:
|
package com.mao.rabbitmq;
import java.io.ByteArrayInputStream; import java.io.ObjectInputStream;
import javax.annotation.Resource;
import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter;
/** * 功能概要:消费接收 * */ public class Consumer implements MessageListener { //private Logger logger = LoggerFactory.getLogger(Consumer.class); @Resource private AmqpTemplate amqpTemplate;
@Override public void onMessage(Message message) { //logger.info("receive message:{}",message); try { //将字节流对象转换成Java对象 Person person=(Person) new ObjectInputStream(new ByteArrayInputStream(message.getBody())).readObject(); System.out.println("年龄:"+person.getAge()); } catch (Exception e) { e.printStackTrace(); } String replyTo = message.getMessageProperties().getReplyTo(); MessageConverter messageConverter=new SimpleMessageConverter();
MessageProperties messageProperties = new MessageProperties(); messageProperties.getHeaders().putAll(message.getMessageProperties().getHeaders());
String response=new String("收到返回消息"); //将Java对象转成Message对象,并作为返回的内容,回送给生产者 Message message2=messageConverter.toMessage(response, messageProperties); amqpTemplate.send(replyTo, message2);
} }
|
Person.java
|
package com.mao.rabbitmq;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L; private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person(String name, int age) { super(); this.name = name; this.age = age; }
}
|
测试类RAbbitmqTest.java:
|
package com.mao.rabbitmq;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RabbitmqTest {
public static void main(String[] args) { ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("application-context.xml"); Proceducer proceducer=(Proceducer) context.getBean("proceducer") ; Person person=new Person("liucc",22); System.out.println(person); proceducer.sendMessage(person);
} } |
参考资料地址:https://www.cnblogs.com/zhangweizhong/p/5687457.html
https://blog.csdn.net/leixiaotao_java/article/details/78924863
浙公网安备 33010602011771号