MQ 全称 Messqge Queue(消息队列),是在消息的传输过程中保存消息的容器。多用与分布式系统之间进行通行。
MQ 相当于一个中间中介,生产方通过 MQ 与消费方交互,他讲应用程序进行耦合
系统的耦合性越高,容错性、可维护性就越低
图片说明: 上图的设计方案,在用户下单之后,订单系统调用库存系统进行库存数据存储,然后调用支付系统付款,然后通过物流系统发送货物等等。这种实际方案就是一个高耦合的设计方案。这种情况下,如果库存系统出现故障,那么整个的调用链路都会因为这个故障而不能继续后续的操作。而且订单下单时需要依赖库存系统时,订单系统相应给客户的就是下单操作失败的响应信息,那么我么就可以说这个设计方案的容错性很低。若是此时。我们在调用以上系统之外,还需要调用其他新增的系统,那么此时我们还需要先修改订单系统的代码,在加入对新增系统的调用,出现这样的情况,我们就可以说这个设计方案的可维护性很低。
使用MQ使得应用间解耦,提升容错性和可维护性。
图片说明: 再加入 MQ 之后,用户的下单操作,订单系统将下单消息放到 MQ 中,后续的库存系统、支付系统等等,都在 MQ 中消费这种订单信息,进行各自系统的业务操作。订单系统不在对其他系统进行直接调用,因此就降低了系统之间的耦合性。并且,某一系统节点的故障并不会影响后续其他系统的正常工作,如库存系统出现故障时,并不会影响支付和物流系统对订单信息的消费,当订单系统将订单信息成功写入 MQ 之后,就可以向客户响应下单操作成功的信息,等库存系统恢复正常之后在对订单信息进行消费,提高了系统的容错性。 需要加入新的系统调用时,只需要消费 MQ 中的信息就能完成新系统的集成,新的系统直接消费 MQ 中的信息,然后执行自己的业务,并不需要对订单系统进行任何的改动,提高了系统的可维护性。
我们以下单操作为例:
图片说明: 用户的下单操作之后,订单系统将数据写入数据库,然后一次调用库存、支付和物流系统。这些操作需要的时间就是 20+300+300+300=920ms;也就是说,一次下单操作正常情况下,需要经理 920ms 之后客户才会收到下单结果的响应信息。
计入MQ之后:
说明: 客户通过订单系统进行下单操作,只需要将数据写入数据库,并将订单信息写入 MQ 队列中,而在写入成功之后,无需再等库存、支付和物流系统的执行结果。就可以直接返回下单操作的响应结果,这些操作耗时 20+5=25ms;
相比 920ms 和 25ms,时间缩短了 895ms,极大的提升了用户体验和系统的吞吐量( 单位时间内处理请求的数目 )。
以订单系统为例,在下单时将订单信息写入数据库,但是数据库每秒只能支撑 1000 左右的并发量,当超过这个阈值时,系统就容易宕机。而我们的系统低峰期时并发在 100 左右,高峰期时可以达到 5000。很明显的高峰期时,数据库肯定及卡死了。
在加入 MQ 之后,消息被 MQ 保存起来,然后系统可以按照自己的消费能力,比如每秒 1000 个消息,从 MQ 中获取消息写入数据库。这样一来,数据库就不会卡死了。
但是,在使用了 MQ 之后,消息消费的速度是 1000,高峰期产生的数据势必会积压在 MQ 中,这样一来订单系统的高峰就被“削”掉了,叫作 “削峰”。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是维持在 1000QPS。直至积压的消息消费完,系统的低谷就被“填”平,这就叫做 “填谷”。
目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、 Kafka、ZeroMQ、MetaMq 等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要 结合自身需求及 MQ 产品特征,综合考虑。
从综合性能来看, RabbitMQ 的综合性能还是比较强的。
AMQP 和 JMS 是 MQ 的两种主流的实现方式。
AMQP: 高级消息队列协议(Advanced Message Queuing Protocol),是一种网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可以传递消息,遵循此协议,不受客户端、中间件产品和开发语言的限制。 2006年,AMQP 规范发布。
JMS: Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件的 API。JMS 是 JavaEE 规范的一种,类比JDBC。很多消息中间件都实现了JMS规范,例如 ActiveMQ。RabbitMQ官方没有提供JMS的实现包,但是开源社区有。
JMS | AMQP |
---|---|
定义统一的接口来对消息进行统一操作 | 通过规定协议来统一数据交互的格式 |
先定了必须使用Java语言 | AMQP只是协议,不规定实现方式,因此是跨语言的 |
规定了两种消息模式 | 丰富多样的消息模式 |
RabbitMQ 官网链接 2007年 Rabbit 公司给予AMQP标准开发的 RabbitMQ 1.0发布。采用Erlang语言开发 。Erlang 语言专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 提供了6种模式:简单模式、work模式、Publish/Subscribe发布与订阅模式、Routing 路由模式、Topics主题模式、RPC远程调用模式(远程调用,暂不做介绍)。
RabbitMQ 官方模式介绍
我们在 CentOS7 的环境下安装 RabbitMQ 服务。
在线安装环境
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
因为 RabbitMQ 是使用 Erlang 语言开发的,所以在安装之前,需要安装 Erlang 的语言环境。
# 安装
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
ps: 安装 Erlang 可能会出现这个错误:
这是由于 GBLIC 的版本太低造成的,可以通过命令查看 GBLIC 的版本:
strings /lib64/libc.so.6 | grep GLIBC
可以看出我的最高版本是 2.17 。所以我安装 Erlang 的时候不会出错。若是出现上面 dos 黑框里面的错误,那就是 GBLIC 的版本太低(最低要求是2.15版本)。
当版本在2.15以下时,就要升级更新版本:
do yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlitedevel readline-devel tk-devel gcc make -y
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/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/myrepoel6/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/myrepoel6/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/myrepoel6/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/myrepoel6/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/myrepoel6/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/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/nscd-2.17-55.el6.x86_64.rpm &
sudo rpm -uvh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps
安装成功以后就可以继续装 Erlang 了。
# 安装
rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps
# 安装
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
# 开启管理界⾯
rabbitmq-plugins enable rabbitmq_management
# 修改默认配置信息
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
# ⽐如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,只保留guest
# {loopback_users, [<<"guest">>]} 修改为 {loopback_users, [guest]},
service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停⽌服务
service rabbitmq-server restart # 重启服务
设置配置文件
cd /usr/share/doc/rabbitmq-server-3.6.5/
cp rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
RabbitMQ 安装好之后,通过 http://ip地址:15672 进行访问; 我们在上面 2.4 中已经释放了 guest 用户。现在我们就可以用 guest登录(默认密码也是 guest)。
登陆成功:
角色说明:
类似于 mysql 中的数据库,并且可以指定用户对库和表等的操作权限。RabbitMQ 也有类似的权限管理,在 RabbitMQ 中可以创建虚拟的消息服务器 VIrtual Hosts ,每个 Virtual Hosts 就是一个相对队里的 RabbitMQ 服务器(就好像一个 mysql 服务中的不同数据库),每个 Virtual Hosts 之间是相互隔离的,exchange、queue、message 不能互通。 Virtual Hosts 名称一般以 / 开头。
进入 RabbitMQ 的管理控制台,找到 Admin 功能,然后找到 Virtual Hosts ,就能看到 Add a new virtual host 的模块。输入Virtual Hosts 名称,然后确定就可以了。
找到创建号的 Virtual Hosts,通过点击其名称进入内部:
进入之后,我们能在页面上看到 Permissions 模块,点开它,找出对用的用户,添加进来,就赋予了对应用户相应的权限。
添加成功:
要是用 RabbitMQ 就要先引入依赖,对应的坐标如下:
// 引入 RabbitMQ 依赖
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.13.1version>
dependency>
简单模式包括了一个生产者、一个消费者和一个消息队列。工作时,生产者向消息队列中投放消息,消费者从消息队列中消费消息:
图解:
<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.0modelVersion>
<groupId>com.kaikebagroupId>
<artifactId>rabbitmq-demoartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.13.1version>
dependency>
dependencies>
project>
package com.kaikeba.rabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 简单模式:生产者
*/
public class Producer {
public static final String QUEUE_NAME = "queue_simple";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// RabbitMQ 主机地址(默认是 localhost)
connectionFactory.setHost("192.168.191.130");
// RabbitMQ 端口(默认是5672)
connectionFactory.setPort(5672);
// RabbitMQ 虚拟主机名称(默认是 / )
connectionFactory.setVirtualHost("/xzk");
// 连接用户名 (默认是 guest)
connectionFactory.setUsername("admin");
// 连接密码 (默认是 guest)
connectionFactory.setPassword("admin");
// 创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
// 创建队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接:是否只能有一个 Consumer 监听消费这个队列
* 参数4:是否在不使用的时候自动删除队列:当没有 Consumer 的时候,是否自动删除这个消息
* 参数5:队列其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 定义发送的信息
String message = "我是发送的简单模式的第一条消息:" + new Random().nextInt();
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
package com.kaikeba.rabbitmq.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 简单模式:消费者
*/
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// RabbitMQ 服务地址
connectionFactory.setHost("192.168.191.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/xzk");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
// 创建连接
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 创建队列
channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
/**
* 接收到消息执行的回调函数
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*super.handleDelivery(consumerTag, envelope, properties, body);*/
// 路由 key
System.out.println("路由 key = " + envelope.getRoutingKey());
// 交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("接收到的消息为:" + new String(body, "utf-8"));
}
};
// 监听消息
channel.basicConsume(Producer.QUEUE_NAME, true, consumer);
}
}
Work Queues 与入门程序的简单模式相比,只是多了若干个消费者(一个或多个),多个消费端共同消费同一个队列中的消息。但是这些消费端之间是竞争关系,也就是说一个消息只能被一个消费端消费。
应用场景: 对于任务过重或任务较多情况使用工作队列,可以有效的提高任务处理速度。
将连接 RabbitMQ 并获取连接的公共部分代码抽取成公共类方法:
package com.kaikeba.work.utils;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 公共方法的抽象
*/
public class RabbitMQUtils {
// 队列名称
public static String WORK_QUEUE_NAME = "work_queue";
/**
* 抽取连接 RabbitMQ 连接的公共方法
*/
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.191.130");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/xzk");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
return connectionFactory.newConnection();
}
}
其实生产者代码和简单模式中的类似,不同之处在于,我们做了简单的改造,让生产者向队列中添加多条消息(我的代码里面添加了30 条)
package com.kaikeba.work.producer;
import com.kaikeba.work.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 工作模式 生产者
*/
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RabbitMQUtils.WORK_QUEUE_NAME, true, false, false, null);
// 模拟发送 20 条消息
for(int i = 1; i <= 30; i++) {
String msg = "RabbitMQ 工作模式 -- " + i;
channel.basicPublish("", RabbitMQUtils.WORK_QUEUE_NAME, null, msg.getBytes());
}
// 关闭资源
channel.close();
connection.close();
}
}
消费者代码,和简单模式中的一模一样,我们在其中打印路由、交换机、消息id和消息内容,只不过,这里要启动两个消费者(再复制一份代码运行):
package com.kaikeba.work.consumer;
import com.kaikeba.work.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 工作模式 消费者1
*/
public class Consumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
//一次只能接收并处理一个消息
channel.basicQos(1);
channel.queueDeclare(RabbitMQUtils.WORK_QUEUE_NAME, true, false, false, null);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 路由 key
System.out.println("路由 key = " + envelope.getRoutingKey());
// 交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
// Consumer02 中只是将下面的 Consumer01 改为 Consumer02
System.out.println("Consumer01 接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(RabbitMQUtils.WORK_QUEUE_NAME, true, consumer);
}
}
相比之前的案例和模式,订阅模式的模型中,多了一个 exchange 部分,相对应的工作流程也就发生了相应的变化:
● P: 生产者。也是生产发送消息的生产者,但是生产的消息是发给 exchange 交换机的;
● C: 消费者。消息的接收者,一直监听队列,等待消费消息。
● **Queue: **消息队列。接收和缓存消息。
● Exchange: 交换机(途中的 X )。一方面,接收生产者发送的消息;另一方面,知道如何的处理消息,比如将消息递交到给某个特别队列、递交给所有队列、或者是将消息丢弃。具体如何操作,取决于交换机( Exchange )的类型。交换机的常见类型有三种:
○ Fanout:广播。将消息传递给所有绑定到交换机的队列;
○ Direct:定向。将消息传递给指定 routing key 的队列;
○ Topic:通配符。把消息交个符合对应 routing pattern(路由模式)的队列。
Exchange(交换机)只负责转发消息,不具备存储消息的能力。 因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,消息就会丢失。订阅模式,因为没有指定 routing key, 所以会将消息转发给绑定到这个交换机上的所有队列,因此,又可这种模式又可以叫做广播模式。
复用 3.2.2.1 工具类 代码。
与 简单模式 和 工作模式 相比多了一个 交换机 的声明部分;
package com.kaikeba.rabbitmq.producer;
import com.kaikeba.rabbitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 发布订阅模式:广播模式 Fanout
*/
public class Producer {
// 创建交换机
public static final String EXCHANGE_FANOUT = "fanout_exchange";
// 创建交换机绑定的两个队列
public static final String EXCHANGE_FANOUT_QUEUE_01 = "fanout_exchange_01";
public static final String EXCHANGE_FANOUT_QUEUE_02 = "fanout_exchange_02";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
// 创建频道
Channel channel = connection.createChannel();
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型:fanout topic direct header(不常用)
*/
channel.exchangeDeclare(EXCHANGE_FANOUT, BuiltinExchangeType.FANOUT);
/**
* 声明队列
* 参数1:队列名称
* 参数2:是否持久化队列
* 参数3:是否独占本次连接,只能有一个 Consumer 监听这个队列
* 参数4:是否自动删除,在不是用的时候自动删除队列
* 参数5:队列其他参数
*/
channel.queueDeclare(EXCHANGE_FANOUT_QUEUE_01, true, false, false, null);
channel.queueDeclare(EXCHANGE_FANOUT_QUEUE_02, true, false, false, null);
// 队列绑定交换机
/**
* 参数1:队列名称
* 参数2:交换机名称
* 参数3:routing key 消息转发时匹配队列的依据
*
* Fanout 是广播机制,会对绑定到交换机上的所有队列全都进行消息分发,所以我们这里可以不指定
*/
channel.queueBind(EXCHANGE_FANOUT_QUEUE_01, EXCHANGE_FANOUT, "");
channel.queueBind(EXCHANGE_FANOUT_QUEUE_02, EXCHANGE_FANOUT, "");
for (int i = 0; i < 20; i++) {
String msg = "rabbit 工作模式的发布订阅模式 广播模式 ------ " + i;
channel.basicPublish(EXCHANGE_FANOUT, "", null, msg.getBytes());
System.out.println("第 " + (i + 1) + "条消息发送成功");
}
// 关闭资源
channel.close();
connection.close();
}
}
消费者也是多出一个队列绑定交换机的过程,我们还是建立两个消费者 Consumer01 和 Consumer02,两个代码一模一样,我们只放 Consumer01 的代码:
package com.kaikeba.rabbitmq.consumer;
import com.kaikeba.rabbitmq.producer.Producer;
import com.kaikeba.rabbitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 工作模式 发布订阅模式 Fanout
*/
public class Consumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 监听队列
channel.queueDeclare(Producer.EXCHANGE_FANOUT_QUEUE_01, true, false, false, null);
// 队列绑定交换机
channel.queueBind(Producer.EXCHANGE_FANOUT_QUEUE_01, Producer.EXCHANGE_FANOUT, "");
// 接收消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("路由 key 为:" + envelope.getRoutingKey());
System.out.println("交换机为:" + envelope.getExchange());
System.out.println("消息 ID 为 :" + envelope.getDeliveryTag() );
System.out.println("消费者 1 接收的消息为:" + new String(body, "UTF-8"));
}
};
// 消费消息
channel.basicConsume(Producer.EXCHANGE_FANOUT_QUEUE_01, true, consumer);
}
}
请添加图片描述
模式图说明:
● P: 生产者。向 Exchange 发送消息,发送时,需指定一个 routing key;
● X: Exchange(交换机)。接收生产者的消息,然后把消息递交给 与 routing key 完全匹配的队列;
● C1: 消费者。其所在队列指定了需要 routing key = error 的消息;
● C2: 消费者。其所在队列指定了需要 routing key 为 info、error 和 warning 的消息。
使用过程:在声明 队列 和交换机时,需要将交换机的类型指定为 Direct,将两者绑定时,需要指定 routing key,然后,生产者将携带有 routing key 信息的消息发送到交换机,交换机根据消息携带的 routing key 去匹配队列的 routing key, 匹配成功之后,就把消息递交到对应的队列中。
我们声明两个队列,对应的 routing 可以 分别是 insert 和 update;
复用 3.2.2.1 工具类 代码。
package com.kaikeba.routing.producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 生产者
*/
public class Producer {
// 交换机
public static final String ROUTING_EXCHANGE = "routing_exchange";
// 队列
public static final String ROUTING_QUEUE_INSERT = "routing_queue_insert";
public static final String ROUTING_QUEUE_UPDATE = "routing_queue_update";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(ROUTING_EXCHANGE, BuiltinExchangeType.DIRECT);
// 声明队列
channel.queueDeclare(ROUTING_QUEUE_INSERT, true, false, false, null);
channel.queueDeclare(ROUTING_QUEUE_UPDATE, true, false, false, null);
// 绑定队列和交换机
channel.queueBind(ROUTING_QUEUE_INSERT, ROUTING_EXCHANGE, "insert");
channel.queueBind(ROUTING_QUEUE_UPDATE, ROUTING_EXCHANGE, "update");
// 生产发送消息
String message = "发送消息, routing key = insert";
channel.basicPublish(ROUTING_EXCHANGE, "insert", null, message.getBytes());
message = "send message,routing key is update";
channel.basicPublish(ROUTING_EXCHANGE, "update", null, message.getBytes());
channel.close();
connection.close();
}
}
Consumer01 消费者 routing key 是insert
package com.kaikeba.routing.consumer;
import com.kaikeba.routing.producer.Producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 消费者01
*/
public class Consumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 交换机
channel.exchangeDeclare(Producer.ROUTING_EXCHANGE, BuiltinExchangeType.DIRECT);
// 队列
channel.queueDeclare(Producer.ROUTING_QUEUE_INSERT, true, false, false, null);
// 交换机绑定队列
channel.queueBind(Producer.ROUTING_QUEUE_INSERT, Producer.ROUTING_EXCHANGE, "insert");
// 回调函数:设置消息处理
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(Producer.ROUTING_QUEUE_INSERT, true, consumer);
}
}
Consumer02 的 routing key 是 update
package com.kaikeba.routing.consumer;
import com.kaikeba.routing.producer.Producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 消费者01
*/
public class Consumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 交换机
channel.exchangeDeclare(Producer.ROUTING_EXCHANGE, BuiltinExchangeType.DIRECT);
// 队列
channel.queueDeclare(Producer.ROUTING_QUEUE_UPDATE, true, false, false, null);
// 交换机绑定队列
channel.queueBind(Producer.ROUTING_QUEUE_UPDATE, Producer.ROUTING_EXCHANGE, "update");
// 回调函数:处理消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(Producer.ROUTING_QUEUE_UPDATE, true, consumer);
}
}
根据开始的图片,我们要是一个队列上有多个 routing key 的时候,应该怎么做呢?就是下面这个图:
我们可以看到,上面的队列的 routing key 是 error,下面的队列的 routing key 是 info、error 和 warning;这个时候我们应该怎么做呢? RabbitMQ 又是怎么处理的呢?我们做了一个测试:绑定队列和交换机的时候我们绑定多个 routing key;如下,然后查看结果:
// 绑定队列和交换机
channel.queueBind(ROUTING_QUEUE_01, ROUTING_EXCHANGE_TEST, "error");
// 绑定交换机和队列
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "info");
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "debug");
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "error");
package com.kaikeba.routing.producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 生产者
*/
public class Producer {
// 交换机
public static final String ROUTING_EXCHANGE_TEST = "routing_exchange_test";
// 队列
public static final String ROUTING_QUEUE_01 = "routing_queue_01";
public static final String ROUTING_QUEUE_02 = "routing_queue_02";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(ROUTING_EXCHANGE_TEST, BuiltinExchangeType.DIRECT);
// 声明队列
channel.queueDeclare(ROUTING_QUEUE_01, true, false, false, null);
channel.queueDeclare(ROUTING_QUEUE_02, true, false, false, null);
// 绑定队列和交换机
channel.queueBind(ROUTING_QUEUE_01, ROUTING_EXCHANGE_TEST, "error");
// 绑定交换机和队列
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "info");
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "debug");
channel.queueBind(ROUTING_QUEUE_02, ROUTING_EXCHANGE_TEST, "error");
// 生产发送消息
String message = "发送消息, com.kaikeba.routing key = insert";
channel.basicPublish(ROUTING_EXCHANGE_TEST, "error", null, message.getBytes());
message = "send message,com.kaikeba.routing key is update";
channel.basicPublish(ROUTING_EXCHANGE_TEST, "info", null, message.getBytes());
channel.close();
connection.close();
}
}
package com.kaikeba.routing.consumer;
import com.kaikeba.routing.producer.Producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 消费者01
*/
public class Consumer01 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 交换机
channel.exchangeDeclare(Producer.ROUTING_EXCHANGE_TEST, BuiltinExchangeType.DIRECT);
// 队列
channel.queueDeclare(Producer.ROUTING_QUEUE_01, true, false, false, null);
// 交换机绑定队列
channel.queueBind(Producer.ROUTING_QUEUE_01, Producer.ROUTING_EXCHANGE_TEST, "insert");
// 回调函数:设置消息处理
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(Producer.ROUTING_QUEUE_01, true, consumer);
}
}
package com.kaikeba.routing.consumer;
import com.kaikeba.routing.producer.Producer;
import com.kaikeba.routing.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 路由模式 消费者01
*/
public class Consumer02 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 交换机
channel.exchangeDeclare(Producer.ROUTING_EXCHANGE_TEST, BuiltinExchangeType.DIRECT);
// 队列
channel.queueDeclare(Producer.ROUTING_QUEUE_02, true, false, false, null);
// 交换机绑定队列
channel.queueBind(Producer.ROUTING_QUEUE_02, Producer.ROUTING_EXCHANGE_TEST, "update");
// 回调函数:处理消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(Producer.ROUTING_QUEUE_02, true, consumer);
}
}
交换机
我们可以看到:当一个队列有多个 routing key 的时候,RabbitMQ 是分别创建绑定关系的,不同的队列可以绑定相同的 routing key,不会冲突,也不会出错。
队列中的消息
队列routing_queue_01:只收到一条消息
队列routing_queue_02:收到了两条消息
● Routing 路由模式需要指定特定的路由 key ;才能将消息发送到对应的队列中;
● 对于有多个 routing key 的队列,我们需要逐次绑定,RabbitMQ 会分别建立绑定关系;
● 不同的队列可以有相同的 routing key,消息发送时,会发送到所有匹配成功的队列。
Topics 类型与 Direct 类型相比,都是根据 routing key 将消息路由到不同的队列。只不过 Topics 模式中 Exchange 可以让队列在绑定 routing key 的时候可以使用通配符。
routing key 一般都是由一个或多个单词组成,单词之间以 “.”分隔,例如“item.insert”、“item.update.insert” 等等;
通配符规则:
● #:匹配一个或多个词;
● *:匹配一个词。
举例:
● item.#:可以匹配到 item.insert.abc 或者 item.insert;
● item.*:只能匹配到 item.insert。
图解:
● 红色 Queue:绑定的是 usa.# 。凡是以 “usa.”开头的 routing key 都会被匹配到;
● 黄色 Queue:绑定的是 #.news 。凡是以 .news结尾的 routing key 都会被匹配到。
package com.kaikeba.topics.producer;
import com.kaikeba.topics.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 通配符模式 生产者
*/
public class Producer {
// 交换机
public static final String TOPICS_EXCHANGE = "topics_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(TOPICS_EXCHANGE, BuiltinExchangeType.TOPIC);
String message = "新增商品。Topics模式:routing key = item.insert";
channel.basicPublish(TOPICS_EXCHANGE, "item.insert", null,message.getBytes());
message = "删除商品。Topics模式:routing key = item.delete";
channel.basicPublish(TOPICS_EXCHANGE, "item.delete", null,message.getBytes());
message = "修改商品。Topics模式:routing key = item.update";
channel.basicPublish(TOPICS_EXCHANGE, "item.update", null,message.getBytes());
channel.close();
connection.close();
}
}
package com.kaikeba.topics.consumer;
import com.kaikeba.topics.producer.Producer;
import com.kaikeba.topics.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 通配符模式 消费者-01
*/
public class Consumer01 {
// 队列
public static final String TOPICS_QUEUE_ALL = "topics_queue_all";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(Producer.TOPICS_EXCHANGE, BuiltinExchangeType.TOPIC);
// 声明队列
channel.queueDeclare(TOPICS_QUEUE_ALL, true, false, false, null);
// 队列交换机绑定
channel.queueBind(TOPICS_QUEUE_ALL, Producer.TOPICS_EXCHANGE, "item.*");
// 回调函数:设置消息处理
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(TOPICS_QUEUE_ALL, true, consumer);
}
}
package com.kaikeba.topics.consumer;
import com.kaikeba.topics.producer.Producer;
import com.kaikeba.topics.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RabbitMQ 通配符模式 消费者-02
*/
public class Consumer02 {
// 队列
public static final String TOPICS_QUEUE_INSERT_UPDATE = "topics_queue_insert_update";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(Producer.TOPICS_EXCHANGE, BuiltinExchangeType.TOPIC);
// 声明队列
channel.queueDeclare(TOPICS_QUEUE_INSERT_UPDATE, true, false, false, null);
// 队列交换机绑定
channel.queueBind(TOPICS_QUEUE_INSERT_UPDATE, Producer.TOPICS_EXCHANGE, "item.update");
channel.queueBind(TOPICS_QUEUE_INSERT_UPDATE, Producer.TOPICS_EXCHANGE, "item.insert");
// 回调函数:设置消息处理
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者02-接收到的消息为:" + new String(body, "utf-8"));
}
};
channel.basicConsume(TOPICS_QUEUE_INSERT_UPDATE, true, consumer);
}
}
因为我们这次的队列声明是放在消费者里面的,所以要先启动消费者,否在生产者生产的消息就会丢失。
Topic主题模式可以实现 Publish/Subscribe发布与订阅模式 和 Routing路由模式 的功能;只是Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
简单模式
一个消费者一个生产者,不需要交换机(实质上使用的是RabbitMQ 默认的交换机,模式是 Direct );
工作队列模式
一个生产者,多个消费者(多个之间是竞争关系);不需要交换机实质上使用的是RabbitMQ 默认的交换机,模式是 Direct );
发布订阅模式 Publish/subscribe
需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定, 当发送消息到交换机后,交换机会将消息发送到绑定的队列 ;
路由模式 Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列 ;
通配符模式 Topic
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列 。