目录
一、MQ的基本概念
1.1MQ概述
1.2MQ的优缺点
1.3MQ的劣势
1.4常见的MQ产品
1.5RabbitMQ简介
1.6JMS
二、RabbitMQ安装和配置
2.1准备安装包
2.2安装依赖环境
2.3安装Erlang
2.4安装RabbitMQ
2.5开启管理界面及配置
2.6RabbitMQ管理控制台使用
三、RabbitMQ快速入门
四、RabbitMQ工作模式
4.1Hello World
4.2Work queues
4.3Pub/Sub订阅模式
4.4Routing路由模式
4.5Topics通配符模式
4.6工作模式总结
五、Spring整合RabbitMQ
六、SpringBoot整合RabbitMQ
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。
多用于分布式系统之间进行通信。
在分布式系统中,通信主要包含两种方式:直接调用和借助第三方进行间接通信。
MQ消息队列就是这个第三方,我们也成为消息中间件,而发送数据的或者提供服务的A系统我们称为生产者,接收数据的B系统我们称为消费者。
MQ的优点主要包括:
1、应用解耦
对于用户下订单这样的应用场景,下订单时需要查询库存,支付状态,物流状态等,如果我们的订单系统直接去调用这些子系统,那么当用户下订单时,会通过订单系统去访问库存系统,若库存系统出bug时,订单系统也会出故障,导致下订单失败。这样整个系统耦合性较高,对应的容错性比较低、可维护性也比较差。
如果我们在中间添加MQ作为中间件,当用户下订单时,会将订单的信息放在MQ消息中间件中,以供库存、支付、物流等系统来消费,当库存系统发生异常时,如果能在短时间内恢复那么也不会影响订单系统。这样提升了系统的解耦,相应的容错性和可维护性大大提高。
2、异步提速
假设还是下订单操作,用户在点击下订单时,首先会访问一下数据库保存数据,接着如果是直接调用,则会同步的依次访问库存、支付、物流等系统,总消耗920ms才可以完成下订单操作。
而如果将MQ作为消息中间件,则仅需要在订单系统访问玩数据库后,将订单的数据发送给MQ消息中间件,供其他系统消费即可,此时就可以直接返回用户下单成功的消息,总消耗25ms。即用户并不关心库存、支付和物流系统中的操作是否执行完成或者是否正确执行。
3、削峰填谷
比如假设某个购物A系统每秒能最大处理1000个请求,到了双十一的时候,请求瞬间增多,此时A系统就极易出现崩溃宕机,导致用户体验较差,
如果我们使用MQ作为中间件,那么用户的请求直接是存储到了MQ中,MQ是可以每秒处理5000个请求的,那么A系统只需要每秒从MQ中拉取1000个请求慢慢处理即可,就不会宕机了。
使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。
使用MQ后,可以提高系统稳定性。
MQ同样还有以下劣势:
既然 MQ 有优势也有劣势,那么使用 MQ 需要满足什么条件呢?
目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征,综合考虑。
RabbitMQ |
ActiveMQ |
RocketMQ |
Kafka |
|
公司/社区 |
Rabbit |
Apache |
阿里 |
Apache |
开发语言 |
Erlang |
Java |
Java |
Scala&Java |
协议支持 |
AMQP,XMPP,SMTP,STOMP |
OpenWire,STOMP,REST,XMPP,AMQP |
自定义 |
自定义协议,社区封装了http协议支持 |
客户端支持语言 |
官方支持Erlang,Java,Ruby等,社区产出多种API,几乎支持所有语言 |
Java,C,C++,Python,PHP,Perl,.net等 |
Java,C++(不成熟) |
官方支持Java,社区产出多种API,如PHP,Python等 |
单机吞吐量 |
万级(其次) |
万级(最差) |
十万级(最好) |
十万级(次之) |
消息延迟 |
微妙级 |
毫秒级 |
毫秒级 |
毫秒以内 |
功能特性 |
并发能力强,性能极其好,延时低,社区活跃,管理界面丰富 |
老牌产品,成熟度高,文档较多 |
MQ功能比较完备,扩展性佳 |
只支持主要的MQ功能,毕竟是为大数据领域准备的。 |
在RabbitMQ中,有一个支持的协议为AMQP,在这里我们首先对这个协议进行介绍。
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。
这里的Exchange(交换机)是用来分发消息的,通过Routes(路由)将消息分发到Queue(队列容器)中存储,而这些消息是由Publisher(生产者)发布到交换机上,最后Consumer(消费者)从队列中监听消息,然后取出消息进行消费。
2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 基础架构如下图:
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
win10系统的安装详细可见:https://blog.csdn.net/weixin_39478524/article/details/122074153
这里我们用centos来做实验,所以以下都是在linux系统上进行安装的。
首先我们下载安装包,
链接:https://pan.baidu.com/s/1P6MceIopN6KEXd9T-TySnA
提取码:n9go
下载以后我们放置到我们的 windows目录下,
然后通过SercureCRT的alt+p命令进行文件传输,输入put -r
传输完成后在根目录下检查,返现该文件夹已经传到虚拟机centos中了,
然后我们就可以开始安装了。
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
在命令行直接输入命令,稍等几分钟即可。
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
首先我们进入到rabbitmq目录,输入命令进行安装,
如果出现如下错误,说明gblic 版本太低,
我们可以查看当前机器的gblic 版本,
strings /lib64/libc.so.6 | grep GLIBC
我的版本是2.2,需要2.15以上,所以需要升级glibc,先使用yum更新安装依赖,
sudo yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make -y
然后下载rpm包,
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-utils-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-static-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-common-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-devel-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-headers-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepo-el6/epel-6-x86_64/glibc-2.17-55.fc20/nscd-2.17-55.el6.x86_64.rpm &
接着安装rpm包,
sudo rpm -Uvh *-2.17-55.el6.x86_64.rpm --force --nodeps
安装完毕后再查看glibc版本,发现glibc版本已经到2.17了
strings /lib64/libc.so.6 | grep GLIBC
rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
然后我们把服务启动一下,
service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停止服务
service rabbitmq-server restart # 重启服务
rabbitmq-plugins enable rabbitmq_management # 开启管理界面
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app # 修改默认配置信息,比如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,只保留guest
我们把"<<>>"去掉,
修改完配置后我们重启一下rabbitmq,
service rabbitmq-server restart
关闭一下防火墙,方便后面我们从windows系统中能访问虚拟机,
systemctl stop firewalld
然后我们输入网址就可以访问,http://192.168.49.200:15672/
默认的登录用户名和密码都是guest,登录之后就可以看到如下的管理控制台界面。
首先我们可以看一下管理控制台的界面,
我们点击Admin管理员,对RabbitMQ可以进行管理,我们可以新建用户,
新建完成之后我们可以查看用户信息,
然后我们点击右边的Virtual Hosts,新建一个虚拟机,
新建完了之后,可以查看到虚拟机的信息,然后我们点击,对aoylaotang用户进行授权,
点击进去之后我们可以在Permissions这一栏设置权限,让aoylaotang可以访问test虚拟机,
回到Users界面,就可以看到aoylaotang可以操作test虚拟机了,
在Overview概述中,有一些文件的路径,其中配置文件提示不存在,
我们进入到虚拟机的rabbitmq安装目录,
cd /user/share/doc/rabbitmq-server-3.6.5/
可以看到有rabbitmq.config.example这个配置文件,
我们把它拷贝到指定的/etc/rabbitmq/的路径下,并改名为rabbitmq.config,
cp ./rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
service rabbitmq-server restart # 重启rabbitmq服务
再刷新页面就可以看到不是not found了,
在Ports and contexts中可以看到三个端口信息,
在Import/export definitions中可以对rabbitmq进行导入导出,需要制定配置文件,
需求:使用简单模式完成消息传递
我们在1.5节简介的时候介绍了RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html,这里我们可以上官网查看一下简单模式对应的图:
从图中我们可以看到只有三部分组成,一个是生产者,一个是消费者,还有一个就是消息队列。
所以简单模式完成消息传递的步骤如下:
第一步,首先我们在一个空工程里面添加生产者和消费者两个模块,
第二步,我们分别在两个子工程中都添加上rabbitmq的依赖,
com.rabbitmq
amqp-client
5.6.0
org.apache.maven.plugins
maven-compiler-plugin
3.8.0
1.8
第三步,我们编写生产者的代码,发送消息数据,
public class Producer_HelloWorld {
public static void main(String[] args) throws IOException, TimeoutException {
//建立与RabbitMQ的连接
//1、创建连接工厂
ConnectionFactory factory=new ConnectionFactory();
//2、设置连接参数
factory.setHost("192.168.49.200");//设置ip地址,默认值为localhost即127.0.0.1
factory.setPort(5672);//设置端口,默认值也为5672
factory.setVirtualHost("/test");//设置虚拟机,默认值为/
factory.setUsername("aoylaotang");//设置用户名,默认值为guest
factory.setPassword("3837");//设置密码,默认值为guest
//3、创建连接
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel=connection.createChannel();
//5、创建队列Queue
/**
* 方法:queueDeclare
* 参数说明
* 1、String queue:队列名称(如果没有该队列则会自动创建)
* 2、boolean durable:是否持久化(当mq重启之后是否还在)
* 3、boolean exclusive:是否独占(只能有一个消费者监听这个队列);当Connection关闭时,是否删除队列
* 4、boolean autoDelete:是否自动删除。当没有Consumer时,自动删除掉
* 5、Map arguments:参数
*/
channel.queueDeclare("helloworld",true,false,false,null);
//6、发送消息到队列中
/**
* 方法:basicPublish
* 参数说明
* 1、String exchange:交换机名称(简单模式下交换机会使用默认的)
* 2、String routingKey:路由名称(简单模式下路由名称需要和队列名称保持一致)
* 3、BasicProperties props:配置信息
* 4、byte[] body:发送的消息数据
*/
String body="hello rabbitmq!";//定义发送的消息数据
channel.basicPublish("","helloworld",null,body.getBytes());
//7、释放资源
channel.close();
connection.close();
}
}
我们运行一下生产者的main方法,然后查看rabbitmq的后台,点击Queues可以看到helloworld的队列被创建成功了,
如果我们不释放Connection和Channel的资源,那么在这个界面也可以看到对应的信息。
第四步,我们创建消费者用来接受数据,消费者同样也要连接到消息队列,所以也要创建connection和channel对象,
public class Consumer_HelloWorld {
public static void main(String[] args) throws IOException, TimeoutException {
//建立与RabbitMQ的连接
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置连接参数
factory.setHost("192.168.49.200");//设置ip地址,默认值为localhost即127.0.0.1
factory.setPort(5672);//设置端口,默认值也为5672
factory.setVirtualHost("/test");//设置虚拟机,默认值为/
factory.setUsername("aoylaotang");//设置用户名,默认值为guest
factory.setPassword("3837");//设置密码,默认值为guest
//3、创建连接
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建队列Queue(在消费者中其实已经创建了,这里也可以省去该部分代码)
/**
* 方法:queueDeclare
* 参数说明
* 1、String queue:队列名称(如果没有该队列则会自动创建)
* 2、boolean durable:是否持久化(当mq重启之后是否还在)
* 3、boolean exclusive:是否独占(只能有一个消费者监听这个队列);当Connection关闭时,是否删除队列
* 4、boolean autoDelete:是否自动删除。当没有Consumer时,自动删除掉
* 5、Map arguments:参数
*/
channel.queueDeclare("helloworld", true, false, false, null);
//6、接受消息
/**
* 方法:basicConsume
* 参数说明
* 1、String queue:队列名称
* 2、boolean autoAck:是否自动确认
* 3、Consumer callback:回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
/**
* 回调方法。当收到消息后,会自动执行该方法
* @param consumerTag:消息表示
* @param envelope:获取信息(例如交换机、路由key等)
* @param properties:配置信息
* @param body:数据
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("consumerTag" + consumerTag);
System.out.println("Exchange" + envelope.getExchange());
System.out.println("RoutingKey" + envelope.getRoutingKey());
System.out.println("properties" + properties);
System.out.println("body" + new String(body));
}
};//创建一个回调对象
channel.basicConsume("helloworld", true, consumer);
//消费者不需要关闭资源,否则无法实时监听消息
}
}
我们运行之后可以发现消息已经被消费者监听到了,
RabbitMQ 一共提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式。详细可以见官网介绍。
其实不同的工作模式之间,就是消息路由的策略和方式不同。
第一个"Hello World"就是我们快速入门中体验到的简单模式,这里不再进行说明了。
Work Queues与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
所以Work queues模式完成消息传递的步骤如下:
其实和简单模式是一样的,就只有个别地方不同,工程和依赖我们之前已经添加好了,直接创建生产者,代码其实大部分也都一样,在创建队列的时候需要修改队列的名称,然后发送消息时我们这次多发几条消息,模拟多任务情况。
//5、创建队列Queue
channel.queueDeclare("workqueues",true,false,false,null);
//6、发送消息到队列中
for (int i = 1; i <= 10; i++) {
String body=i+":hello rabbitmq!";//定义发送的消息数据
channel.basicPublish("","workqueues",null,body.getBytes());
}
然后我们创建消费者,相对于简单模式,我们需要修改队列名称,然后接受消息时我们只输出消息数据,
//5、创建队列Queue(在消费者中其实已经创建了,这里也可以省去该部分代码)
channel.queueDeclare("workqueues", true, false, false, null);
//6、接受消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("body" + new String(body));//只输出消息信息
}
};//创建一个回调对象
channel.basicConsume("workqueues", true, consumer);
这里我们创建两个消费者,代码完全相同,分别启动这两个消费者进行消息监听,
然后我们启动生产者发送10条消息数据看看输出情况,
可以看到这两个消费者是按序消费的,消费者1处理一条消息,然后消费者2处理一条消息,以此类推。
1. 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
2. Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
我们创建一个生产者来发送信息,前四步跟前面一样,首先创建连接工厂,设置连接参数,创建连接以及创建channel,然后我们此时需要把消息发送到交换机上,所以接下来我们要创建交换机,
public class Producer_PubSub {
public static void main(String[] args) throws IOException, TimeoutException {
//建立与RabbitMQ的连接
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置连接参数
factory.setHost("192.168.49.200");//设置ip地址,默认值为localhost即127.0.0.1
factory.setPort(5672);//设置端口,默认值也为5672
factory.setVirtualHost("/test");//设置虚拟机,默认值为/
factory.setUsername("aoylaotang");//设置用户名,默认值为guest
factory.setPassword("3837");//设置密码,默认值为guest
//3、创建连接
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/**
* 方法名:exchangeDeclare
* 参数说明
* 1、String exchange:交换机名称
* 2、BuiltinExchangeType type:交换机类型
* DIRECT("direct"):定向
* FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定的队列
* TOPIC("topic"):通配符方式
* HEADERS("headers"):参数匹配
* 3、boolean durable:是否持久化
* 4、boolean autoDelete:是否自动删除
* 5、boolean internal:是否内部使用,一般false
* 6、Map arguments:参数
*/
String exchangeName = "test_fanout";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
//6、创建队列
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
//7、绑定队列和交换机
/**
* 方法名:queueBind
* 参数说明
* 1、String queue:队列名称
* 2、String exchange:交换机名称
* 3、String routingKey:路由键(绑定规则)
* 如果交换机的类型为fanout,则routingKey设置为空字符串""
*/
channel.queueBind(queue1Name, exchangeName, "");
channel.queueBind(queue2Name, exchangeName, "");
//8、发送消息
String body = "日志信息:aoy调用了service方法...日志级别:info...";//模拟一条日志信息数据
channel.basicPublish(exchangeName, "", null, body.getBytes());
//9、释放资源
channel.close();
connection.close();
}
}
我们在rabbitmq管理界面可以看到交换机已经被创建好了,
点进去可以查看交换机的绑定关系,可以看到该交换机绑定了两个队列,
进入队列界面可以看到对应的队列也创建好了,并且交换机已经把数据发送到了绑定的队列中,
消费者的代码基本都相同,不同的是监听的队列名不同,以及收到消息后的执行方法,
我们创建消费者1监听队列1的消息,并将日志信息打印到控制台(这里直接模拟操作即可),接着创建消费者2监听队列2的消息,并将日志信息保存到数据库(同样是模拟操作)。
//6、接受消息
/**
* 方法:basicConsume
* 参数说明
* 1、String queue:队列名称
* 2、boolean autoAck:是否自动确认
* 3、Consumer callback:回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
/**
* 回调方法。当收到消息后,会自动执行该方法
* @param consumerTag:消息表示
* @param envelope:获取信息(例如交换机、路由key等)
* @param properties:配置信息
* @param body:数据
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("body" + new String(body));
System.out.println("将日志信息打印到控制台...");
}
};//创建一个回调对象
channel.basicConsume(queue1Name, true, consumer);//监听队列1的消息
//消费者不需要关闭资源,否则无法实时监听消息
//6、接受消息
/**
* 方法:basicConsume
* 参数说明
* 1、String queue:队列名称
* 2、boolean autoAck:是否自动确认
* 3、Consumer callback:回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
/**
* 回调方法。当收到消息后,会自动执行该方法
* @param consumerTag:消息表示
* @param envelope:获取信息(例如交换机、路由key等)
* @param properties:配置信息
* @param body:数据
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("body" + new String(body));
System.out.println("将日志信息存储到数据库...");
}
};//创建一个回调对象
channel.basicConsume(queue2Name, true, consumer);//监听队列2的消息
//消费者不需要关闭资源,否则无法实时监听消息
然后我们启动这两个消费者,查看输出信息。
可以看到两个消费者都监听到了同一条消息,并作出了不同的响应。
在Routing路由模式中,队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey。
并且消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey,
Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息。
在上面的例子中,我们将info级别的日志信息都存储到了数据库中,但是这样会占用数据库的空间,所以一般我们只存储比较重要的日志信息,比如error级别的日志,所以我们希望对于一般的日志信息,让消费者2直接打印在控制台上,而碰到error级别的日志,则让消费者1存储到数据库。
生产者的代码主要从创建交换机开始不同,我们需要创建DIRECT类型的交换机(即Routing模式),然后在绑定队列和交换机时需要指定routingKey路由键,然后发送消息时也要指定对应的routingKey,这样交换机就知道该消息需要发送到哪个队列中去。
//5、创建交换机
/**
* 方法名:exchangeDeclare
* 参数说明
* 1、String exchange:交换机名称
* 2、BuiltinExchangeType type:交换机类型
* DIRECT("direct"):定向
* FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定的队列
* TOPIC("topic"):通配符方式
* HEADERS("headers"):参数匹配
* 3、boolean durable:是否持久化
* 4、boolean autoDelete:是否自动删除
* 5、boolean internal:是否内部使用,一般false
* 6、Map arguments:参数
*/
String exchangeName = "test_direct";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);//定义交换机的类型为定向,即Routing模式
//6、创建队列
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
//7、绑定队列和交换机
/**
* 方法名:queueBind
* 参数说明
* 1、String queue:队列名称
* 2、String exchange:交换机名称
* 3、String routingKey:路由键(绑定规则)
* 如果交换机的类型为fanout,则routingKey设置为空字符串""
*/
//队列1绑定error
channel.queueBind(queue1Name, exchangeName, "error");
//队列2绑定info、error、warning
channel.queueBind(queue2Name, exchangeName, "info");
channel.queueBind(queue2Name, exchangeName, "error");
channel.queueBind(queue2Name, exchangeName, "warning");
//8、发送消息
String infoBody = "日志信息:aoy调用了service方法...日志级别:info...";//模拟一条info级别日志信息数据
channel.basicPublish(exchangeName, "info", null, infoBody.getBytes());
String errorBody = "日志信息:Failed to load class XXXX...日志级别:error...";//模拟一条error级别日志信息数据
channel.basicPublish(exchangeName, "error", null, errorBody.getBytes());
String warningBody = "日志信息:变量XXX没有被使用...日志级别:warning...";//模拟一条warning级别日志信息数据
channel.basicPublish(exchangeName, "warning", null, warningBody.getBytes());
//9、释放资源
channel.close();
connection.close();
在生产者中,我们发送了三条消息,分别为info、error和warning级别的,然后我们启动生产者。
可以看到队列1中只有1条消息,队列2中有3条消息,
接着我们创建消费者,让消费者1监听队列1的消息,并将监听到的日志信息保存到数据库;消费者2监听队列2的消息,并将监听到的日志信息打印到控制台。
//5、创建队列Queue(在消费者中其实已经创建了,这里也可以省去该部分代码)
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
//6、接受消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("body" + new String(body));
System.out.println("将日志信息保存到数据库...");
}
};//创建一个回调对象
channel.basicConsume(queue1Name, true, consumer);//监听队列1的消息
//5、创建队列Queue(在消费者中其实已经创建了,这里也可以省去该部分代码)
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
//6、接受消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
System.out.println("body" + new String(body));
System.out.println("将日志信息打印到控制台...");
}
};//创建一个回调对象
channel.basicConsume(queue2Name, true, consumer);//监听队列2的消息
然后我们启动这两个消费者,可以看到消费者1只处理类error级别的日志消息,
消费者2处理了info、error和warning级别的日志消息,
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符。
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词。
例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
红色 Queue:绑定的是 usa.# ,因此凡是以 usa. 开头的 routing key 都会被匹配到
黄色 Queue:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配
绿色 Queue:绑定的是 #.weather ,因此凡是以 .weather 结尾的 routing key 都会被匹配
蓝色 Queue:绑定的是 europe.# ,因此凡是以 europe. 开头的 routing key 都会被匹配到
所以如果routing key为usa.news,则红色和黄色队列都可以收到该消息。
下面则是Topics消息传递的示意图:
假设我们认为定义routing key由两部分组成:系统的名称.日志的级别。
我们想将order订单系统的所有日志信息以及任意系统的error级别信息存入数据库,而所有的日志信息都可以打印在控制台。
首先是创建生产者,大部分代码和Routing模式相同,但是需要修改交换机的类型,队列和交换机的绑定方式等,
//5、创建交换机
String exchangeName = "test_topic";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);//定义交换机的类型为通配符方式,即Topics模式
//6、创建队列
String queue1Name = "test_topic_queue1";
String queue2Name = "test_topic_queue2";
channel.queueDeclare(queue1Name, true, false, false, null);
channel.queueDeclare(queue2Name, true, false, false, null);
//7、绑定队列和交换机
//队列1绑定error级别日志信息和order系统的所有级别日志信息
channel.queueBind(queue1Name, exchangeName, "#.error");
channel.queueBind(queue1Name, exchangeName, "order.*");
//队列2绑定所有的日志信息
channel.queueBind(queue2Name, exchangeName, "*.*");
//8、发送消息
String orderinfoBody = "日志信息:aoy调用了service方法...日志级别:info...";//模拟一条order系统info级别日志信息数据
channel.basicPublish(exchangeName, "order.info", null, orderinfoBody.getBytes());
String usererrorBody = "日志信息:Failed to load class XXXX...日志级别:error...";//模拟一条user系统error级别日志信息数据
channel.basicPublish(exchangeName, "user.error", null, usererrorBody.getBytes());
String userwarningBody = "日志信息:变量XXX没有被使用...日志级别:warning...";//模拟一条user系统warning级别日志信息数据
channel.basicPublish(exchangeName, "user.warning", null, userwarningBody.getBytes());
//9、释放资源
channel.close();
connection.close();
我们发送了三条日志信息,分别为order系统的info级别日志、user系统的error级别日志、user系统的warning级别日志。我们启动一下生产者,看到队列1接收到了2条消息,队列2接收了3条消息。
然后我们创建消费者,还是让消费者1监听队列1的消息,并将消息存储到数据库,让消费者2监听队列2的消息,并将消息打印到控制台,代码和Routing的差不多,改一下监听的队列名称就行。然后我们启动消费者,查看输出,
消费者1监听到了order.info和user.error的日志消息,
消费者2监听到了所有的日志消息,
需求:使用Spring整合RabbitMQ
步骤:
首先我们创建生产者和消费者工程,
然后我们对两个工程都添加上对应的依赖以及编译插件,
org.springframework
spring-context
5.1.7.RELEASE
org.springframework.amqp
spring-rabbit
2.1.8.RELEASE
junit
junit
4.12
org.springframework
spring-test
5.1.7.RELEASE
org.apache.maven.plugins
maven-compiler-plugin
3.8.0
1.8
接着我们需要对rabbitmq进行配置,创建rabbitmq.properties配置文件,主要包括rabbitmq的主机地址,端口号,用户名密码以及虚拟机名等信息,将配置文件放在资源文件夹下,
rabbitmq.host=192.168.49.200
rabbitmq.port=5672
rabbitmq.username=aoylaotang
rabbitmq.password=3837
rabbitmq.virtual-host=/test
然后是创建spring-rabbitmq-producer.xml来读取rabbitmq的配置文件,并通过配置的方式来创建连接、交换机、队列等。
然后我们定义一个测试类,利用RabbitmqTemplate的convertAndSend方法,以简单模式来给spring_queue队列发送消息,
@RunWith(SpringJUnit4ClassRunner.class)//使用Spring容器测试环境
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")//加载配置文件
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;//注入RabbitTemplate对象
@Test//使用简单模式发送消息
public void testHelloWorld(){
String message="hello world!";
rabbitTemplate.convertAndSend("spring_queue",message);//向spring_queue队列发送消息
}
}
打开rabbitmq控制台,我们可以看到交换机以及队列都被创建好了(在配置文件中我们都设置的是自动创建),然后我们看到spring_queue队列中也接收到了我们发送的消息。
然后我们测试一下使用订阅模式,即广播模式来发送消息,广播模式需要发送消息到指定的交换机,routing key设置为空,这样与交换机绑定的所有队列都能收到消息,
@Test//使用订阅(广播)模式发送消息
public void testFanout(){
String message="fanout message";
rabbitTemplate.convertAndSend("spring_fanout_exchange","",message);//向spring_fanout_exchange交换机发送消息,与该交换机绑定的队列都能收到消息
}
运行一下,打开rabbitmq后台查看,绑定的两个队列都受到了消息,
接着我们测试通配符模式(Routing方式其实和通配符模式差不多,只不过通配符的routing key可以使用通配符*和#,本质都是让交换机通过routing key来决定哪个消息发送到哪个队列)
@Test//使用通配符模式发送消息
public void testTopic(){
String message1="topic message1";
String message2="topic message2";
String message3="topic message3";
//生产者先将消息发送到交换机,然后交换机通过routing key将消息发送到指定队列
rabbitTemplate.convertAndSend("spring_topic_exchange","java.exe",message1);
rabbitTemplate.convertAndSend("spring_topic_exchange","java.1.exe",message2);
rabbitTemplate.convertAndSend("spring_topic_exchange","c++.exe",message3);
}
按道理来说,spring_topic_queue_star会收到第一条消息,spring_topic_queue_well会受到第一第二两条消息,spring_topic_queue_well2会受到第三条消息。
我们启动运行一下,看一下果然spring_topic_queue_well收到了两条消息,其他队列各收到了一条消息,后面我们可以通过消费者取出消息来进一步验证是否正确。
写完了生产者,接下来我们写一下消费者,首先也是要对消费者工程进行配置,我们把rabbitmq.properties配置文件直接复制过来,然后创建spring-rabbitmq-consumer.xml配置文件,
这里的监听器类都是需要我们自己定义的,以SpringQueueListener类为例,我们这里直接打印监听到的消息,(其他监听器类代码完全相同,只是类名不同)
public class SpringQueueListener implements MessageListener {
@Override//回调方法,监听到消息后执行的方法
public void onMessage(Message message) {
System.out.println("spring_queue队列消息:"+new String(message.getBody()));//打印监听到的消息
}
}
都写好之后我们创建一个测试类,加载配置文件进行队列消息监听,查看输出:
验证了一下都是正确的。
在使用SpringBoot整合RabbitMQ时,我们分两个部分进行整合:生产者和消费者。
生产者的整合步骤如下:
1. 创建生产者SpringBoot工程
2. 引入starter依赖坐标
3. 编写yml配置,基本信息配置
4. 定义交换机,队列以及绑定关系的配置类
5. 注入RabbitTemplate,调用方法,完成消息发送
首先我们创建一个SpringBoot的消费者工程,
然后第二步引入对应的依赖,
org.springframework.boot
spring-boot-starter-parent
2.1.4.RELEASE
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-test
然后我们创建一个rabbitmq的配置文件application.yml,
# 配置rabbitmq的基本信息spring:spring:
spring:
rabbitmq:
host: 192.168.49.200
port: 5672
username: aoylaotang
password: 3837
virtual-host: /test
接着我们定义一个启动类ProducerApplication,
@SpringBootApplication//标志当前类为SpringBoot启动类
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class);
}
}
然后是创建一个rabbitmq的配置类RabbitMQConfig,配置交换机、队列及对应的绑定关系。
@Configuration//标志当前类为配置类
public class RabbitMQConfig {
public static final String EXCHANGE_NAME="boot_topic_exchange";//定义交换机的名字常量
public static final String QUEUE_NAME="boot_queue";//定义队列的名字常量
//配置交换机
@Bean("bootExchange")//将当前方法的返回值作为Bean对象注入Spring容器中
public Exchange bootExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();//创建一个topic类型的交换机
}
//配置队列
@Bean("bootQueue")//将当前方法的返回值作为Bean对象注入Spring容器中
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();//创建队列
}
//配置队列和交换机的绑定关系
@Bean
//以参数的方式将交换机和队列注入到该方法中,并且指定Bean对象的名称
public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();//将queue队列绑定到exchange交换机,通配符为boot.#,并且没有参数
}
}
然后我们创建一个测试类,使用RabbitTemplate对象来发送消息到交换机,并由交换机分发到队列
@SpringBootTest//检索配置文件以及配置类,并加载所有被管理的Bean对象
@RunWith(SpringRunner.class)
public class ProducerTest {
//注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.666","springboot rabbitmq!");//向交换机发送消息
}
}
启动测试,打开rabbitmq控制台,查看boot_queue队列已经创建好了,并且也接收到了消息,
我们的生产者这一端就创建好了。
消费者的整合步骤如下:
1. 创建消费者SpringBoot工程
2. 引入starter依赖坐标
3. 编写yml配置,基本信息配置
4. 定义监听类,使用@RabbitListener注解完成队列监听。
首先我们要创建SpringBoot消费者工程,
同样的我们还是引入依赖坐标(和生产者工程一样),还有对应的rabbitmq的配置信息,
然后我们创建一下SpringBoot的引导类ConsumerApplication,
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
都配置好了之后我们开始定义监听类RabbitMQListener,
@Component//将当前类实例化到spring容器中
public class RabbitMQListener {
@RabbitListener(queues = "boot_queue")//监听boot_queue队列的消息,并将消息封装到方法参数message中
public void ListenerQueue(Message message){
System.out.println(message);
}
}
然后我们启动引导类的main方法,启动整个SpringBoot工程,查看输出,
可以看到消息成功从指定队列取出来了
小结: