RabbitMQ笔记(一)
MQ
Message Queue 消息队列,也称消息中间件,是一个以队列这种“先进先出”的基本数据结构为基础的软件
。其遵守java中JMS规范,通过提供接口的方式进行不同服务之间的消息传递,其消息传递的方式大致如下:
下面简单提一下在本次笔记中可能用到的一些消息中间件概念的来源,其中很多概念都是来自JMS规范的。
详情可以看博文JMS消息服务JMS规范及原理详解。
JMS规范(可跳过)
Java Message Service java消息服务应用程序接口
,是一个在java平台中面向消息中间件
的API接口,类似于JDBC接口是用来与数据库进行交互的AIP接口一样,JMS用在两个应用程序(例如两个springboot项目之间)或者分布式系统中进行异步信息通信。下面简单提到JMS规范中和消息队列相关的基本概念。从后面的消息队列中可以看到这些概念的落地。
- 构成对象
-
ConnectFactory 连接工厂对象。
-
Connection 连接对象。
- 构成要素
- 消费者:消费消息的对象。
- 生产者:生产消息的对象。
- 客户端:在两个或多个应用程序之间接收或发送消息的对象。
- 队列:保存消息的对象,存在于客户端中。
消息队列的应用场景
应用解耦
,异步消息
,流量削峰
。后面会详细介绍这三种应用场景的详细用途。
市场上主流的MQ产品
- ActiveMQ
RabbitMQ
:比Kafka稳定,但是性能没有其高,与spring天然集成。- Kafka:一般用于大数据的日志处理中,性能高,稳定性没有RabbitMQ高。
- 阿里巴巴的RocketMQ
下面是RabbitMQ的详细内容。
RabbitMQ
RabbitMQ是一个实现了高级消息队列协议(AMQP)的面向消息的中间件,是由Erlang语言编写的,稳定性高,不容易丢失数据的软件。能够和spring框架进行天然的集成
,因此使用非常广泛。
-
AMQP协议
AMQP协议 Advance Message Queue Protocol,用于解决不同平台之间消息传递交互问题,是一种链接协议,和JMS有着本质的区别。AMQP协议不是从API层(JMS是从API层)而是从网络层直接定义数据交换的格式。其工作模式如下,这和后面的RabbitMQ工作模式图几乎是一样的:
- Erlang语言:
一种面向并发的编程语言。
- RabbitMQ的工作模式:
大致的工作流程如下:
生产者(可以java中的类)生产消息后通过通道(channel)将消息发送到交换机(Exchange)中,交换机再通过一定的规则和消息队列(queue)进行关联,最后消费者通过通道从队列中取出数据进行消费。
RabbitMQ的安装
上面说了RabbitMQ可以看成是一款软件,所以需要我们进行安装:
-
先安装Erlang语言
根据自己的系统下载对应的文件,我这里是window10的,下载完之后点击opt_win64_23.2.exe
进行下载安装,安装之后可以将安装之后的bin目录添加到环境变量
中,在cmd中输入erl
,如果有版本信息提示则证明安装成功。
-
安装RabbitMQ
进入官网后往下找回看到这个,点击下载就行了,不过我们国内访问可能会有点慢。
安装完后同样将安装后的bin目录添加进环境变量。
-
激活RabbitMQ Management Plugin可视化插件
在安装文件夹下进入sbin目录,并在该目录打开命令行,执行命令
c:\User>rabbitmq-plugins.bat enable rabbitmq_management
-
插件安装成功后访问
http:localhost:15672
,输入账号和密码均为:guest。看到如下界面
RabbitMQ的模式
新版的RabbitMQ总共有7种模式,详情可以在在官网教程中看到,各种语言的教程都有,下面只讲在java环境下的前面五种java代码及其整合spring boot后的使用方式(与spring框架天然集成,使用非常方便)。
直连模式 Hello World
这是最简单的一种模式,生产者通过直接将消息放在队列中,不用通过交换机
,之后再由消费者直接进行消费。
P:provider C:consumer
-
java代码未封装示例:
生成者端代码
Provider.java
// 生产者端代码 public class Provider { @Test public void testSendMessage() throws IOException, TimeoutException { // 配置连接信息 // 获取ConnectionFactory工厂对象 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置RabbitMQ软件运行的主机 connectionFactory.setHost("localhost"); // 设置端口,使用消息队列的服务端使用该端口,15672用在浏览器访问中 connectionFactory.setPort(5672); // 设置虚拟主机的名称,虚拟主机可以在RabbitMQ UI界面中进行创建 connectionFactory.setVirtualHost("demo1"); // 设置用户名称,同样可以在UI界面中进行创建 connectionFactory.setUsername("demo1User"); connectionFactory.setPassword("123"); // 获取连接对象 Connection connection = connectionFactory.newConnection(); // 获取连接通道 Channel channel = connection.createChannel(); // 绑定队列- 如果队列还没有创建,则会自动创建 channel.queueDeclare("demo1Queue", true, false, false, null); // 往队列中生产消息 channel.basicPublish("", "demo1Queue", null, "hello rabbitmq".getBytes(StandardCharsets.UTF_8)); channel.close(); connection.close(); } }
-
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) 参数说明
参数 说明 queue 队列名称,如果该队列还没有创建则会自动创建 durable 是否将队列进行持久化,设置为true会将队列持久化进磁盘中,如果设置为false时,当RabbitMQ-server关闭时队列会被删除。 注意这里并不是指发布的消息进行持久化,数据持久化在channel.basicPublish()中进行设置。
exclusive 该连接是否独占该队列,设置为true时别的连接不能使用到该队列。一般设置为false。 autoDelete 是否在队列为空,没有消费者与该队列连接时自动删除该队列。 -
channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) 参数说明
参数 | 说明 |
---|---|
exchange | 交换机名称,在后面的模式中会用到,直连模式中没有用到。 |
routingKey | 消息要发送到的队列名称。 |
props | 设置为:MessageProperties.PERSISTENT_TEXT_PLAIN时会对消息进行持久化。 |
body | 要发送的消息数据,为字节数组类型。 |
消费者端代码consumer.jva
// Consumer.java
public class Consume {
public static void main(String[] args) throws IOException, TimeoutException {
// 设置连接信息
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setPort(5672);
connectionFactory.setHost("localhost");
connectionFactory.setVirtualHost("demo1");
connectionFactory.setUsername("demo1User");
connectionFactory.setPassword("123");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
// 设置连接队列,注意这里要和生成者中的代码设置一样
channel.queueDeclare("demo1Queue", true, false, false, null);
channel.basicConsume("demo1Queue", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("message = " + new String(body));
}
});
}
}
- channel.basicConsume(String queue, boolean autoAck, Consumer callback)参数说明
参数 | 说明 |
---|---|
queue | 消费信息的队列名称。 |
autoAck | 是否自动确认消息已经消费,如果为false则需要手动确认消费信息。 |
callback | 回调,信息消费的处理逻辑在该匿名类中执行。重写里面的handleDelivery方法可以获得要消费的数据。 |
补充
在讲下面的模型时,先将创建连接的代码封装到RabbitMQUtils.java
中,如下所示:
/**
* Rabbit MQ 工具类
* @author zhangshaolong
*/
public class RabbitMQUtils {
private static ConnectionFactory connectionFactory;
private RabbitMQUtils(){}
static {
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("demo1");
connectionFactory.setUsername("demo1User");
connectionFactory.setPassword("123");
}
/**
* 获取连接
* @return
*/
public static Connection getConnection() {
if (connectionFactory != null) {
try {
return connectionFactory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 关闭通道和连接对象
* @param channel
* @param connection
*/
public static void closeChannelAndConnection(Channel channel, Connection connection) {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
工作队列
Work Queue,或者称为Task Queue, 任务模型。在某些场景下可能会导致生产的消息比消费的消息要多,这个时候就可以让多个消费者共享一个消息队列,每当一条消息被消费的时候就会消失,避免任务的重复执行。
-
java代码示例
生产者端循环生产10条消息:
// Provider.java public class Provider { public static void main(String[] args) throws IOException { // 从上面封装的工具类中获取连接 Connection connection = RabbitMQUtils.getConnection(); Channel channel = connection.createChannel(); channel.queueDeclare("work", true, false, false, null); for (int i = 0; i < 10; i++) { channel.basicPublish("", "work", null, (i + "work queue").getBytes(StandardCharsets.UTF_8)); } RabbitMQUtils.closeChannelAndConnection(channel, connection); } }
消费者1和消费者2的代码是一样的:
// Consumer1.java / Consumer2.java public class Consumer1[Consumer2] { public static void main(String[] args) throws IOException { Connection connection = RabbitMQUtils.getConnection(); Channel channel = connection.createChannel(); channel.queueDeclare("work", true, false, false, null); channel.basicConsume("work", true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("Consumer1 = " + new String(body)); } }); } }
这种方式的输出如下所示:
// Consumer1 Consumer1 = 0work queue Consumer1 = 2work queue Consumer1 = 4work queue Consumer1 = 6work queue Consumer1 = 8work queue // Consumer2 Consumer2 = 1work queue Consumer2 = 3work queue Consumer2 = 5work queue Consumer2 = 7work queue Consumer2 = 9work queue
可见是两个消费者轮流消费队列中的消息的,这是工作模式中默认的消费方式。
在默认情况下,工作模式会采用轮询的方式给每个消费者平均分配消息。
消息确认机制
上面两种模式所channel.basicConsume()方法中的第二个参数autoAck都设置为true,即程序运行一进入该方法中就会被确认已经被消费者消费了,接着消息队列就会将消息从队列中删除。但是如果在消息的处理逻辑中的程序(回调函数中handleDelivery方法)出现异常,消息并没有真正被消费,同时此时该消息已经被队列删除了,就会导致消息的丢失。
因此可以将该参数设置为false,进而手动进行消息消费完毕的确认。Consumer.java修改之后的代码如下所示:
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare("work", true, false, false, null);
// 服务器传送的最大消息数量,设置为1
channel.basicQos(1);
// autoAck参数设置为false
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 = " + new String(body));
// 手动进行消费
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
发布订阅模型 - 广播模型
fanout 扇出或称广播,在这种模式中需要我们设置交换机
,并且会为每个消费生成临时队列,这些临时队列为消费者所独有。同时这些临时队列中共享交换机中的数据。即同个消息可以有多个消费者使用,注意在上面的两种模式中一条消息只能被一个消费者所消费。
-
java代码示例
生成者代码,在有交换机参与的代码中只需要添加一步设置交换机即可。
// Provider.java public class Provider { public static void main(String[] args) { // 1 获取连接 Connection connection = RabbitMQUtils.getConnection(); // 2 获取连接通道 Channel channel = connection.createChannel(); // 3 设置交换机 channel.exchangeDeclare("log", "fanout"); // 4 生成消息 channel.basicPublish("log", "", null, "hello fanout pattern".getBytes(StandardCharsets.UTF_8)); // 关闭连接 RabbitMQUtils.closeChannelAndConnection(channel, connection); } }
- channel.exchangeDeclare(String exchange, String type)参数说明
参数 | 说明 |
---|---|
exchange | 交换机名称 |
type | 模式类型,在广播模式中设置为“fanout” |
消费者代码,由于广播模式中需要给每个消费者创建临时队列,因此在消费者代码中除了要设置交换机外还要创建临时队列,并且将这个临时队列和交换机进行绑定。
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtils.getConnection();
final Channel channel = connection.createChannel();
// 设置交换机
channel.exchangeDeclare("log", "fanout");
// 创建临时队列
String queue = channel.queueDeclare().getQueue();
// 绑定队列
channel.queueBind(queue, "log", "");
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumer1 = " + new String(body));
channel.basicAck(envelope.getDeliveryTag(), true);
}
});
}
}
-
channel.queueBind(String queue, String exchange, String routingKey)参数说明
参数 说明 queue 临时队列名称 exchange 交换机名称 routingKey 路由key,用在下面的路由模式和动态路由模式中
路由模型
routing 路由订阅模型
广播模型会将所有的消息都共享给所有的临时队列,但是有时候可能需要区分消费者能够消费的消息类型,这个时候就要用到订阅模型。例如在日志系统中,error级别的要持久化到磁盘中,而info,error,warning等级别的只要打印到控制台就行了,这个时候就可以使用订阅模型对不同的消费者能够消费的消息类型进行区分。
-
java代码实现
生成者端,基本和广播模型是一致的,只需要在发布消息进行设置路由key就行了,这里的路由key可以简单看成就是消息的类型。只有在消费者端设置同样的key才能消费该消息。
// Provider.java public class Provider { public static void main(String[] args) { // 1 获取连接 Connection connection = RabbitMQUtils.getConnection(); // 2 获取连接通道 Channel channel = connection.createChannel(); // 3 设置交换机 channel.exchangeDeclare("log", "direct"); // 4 生成消息 - 并设置路由key channel.basicPublish("log", "info", null, "hello direct pattern".getBytes(StandardCharsets.UTF_8)); // 关闭连接 RabbitMQUtils.closeChannelAndConnection(channel, connection); } }
路由模式中需要将channel.exchangeDeclare中的第二个参数设置为
“direct"
.消费者端,在消费中想要消费的信息类型也要和路由进行绑定并且将交换机的模式设置为“direct”。
public class Consumer1 { public static void main(String[] args) throws IOException { // 获取连接 Connection connection = RabbitMQUtils.getConnection(); final Channel channel = connection.createChannel(); // 设置交换机 channel.exchangeDeclare("log", "direct"); // 创建临时队列 String queue = channel.queueDeclare().getQueue(); // 绑定队列 channel.queueBind(queue, "log", "info"); channel.basicConsume(queue, false, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("consumer1 = " + new String(body)); channel.basicAck(envelope.getDeliveryTag(), true); } }); } }
话题模型
Topic模型,也称动态路由模型。与订阅模型相比,动态路由模型可以通过通配符的方式类配置routing key的名称。
(来源:RabbitMQ官网)
这种模式下建议routing key的命名方式为:name1.name2
。
通配符的规则如下:
通配符 | 作用 |
---|---|
* | 匹配一个单词 |
# | 匹配零个或多个单词 |
-
java代码示例
生成者端,只需要修改一下命名规则和交换机的模式即可。
// 设置为topic模式 channel.exchangeDeclare("log", "topic"); String routingKey = "user.save"; channel.basicPublic(queue, routingKey, null, data.getByte());
在这种模式中交换机的模式要设置为:
“topic”
。消费者端代码
// 设置为topic模式 channel.exchangeDeclare("log", "topic"); // 定义路由key的规则 String routingKey = "*.save"; String routingKey2 = "user.#"; // 可以匹配user.save, user1.save等 channel.queueBind(queue, "log", routingkey); // 可以匹配 user.delete.save等任意多个单词 // channel.queueBind(queue, "log", "routingKey2");
消息队列的应用场景
异步处理
场景:用户注册后,可能需要发送注册邮件和短信,传统的方式有串行方式和并行方式
- 串行方式: 将注册信息写入数据库后,发送邮件后再发送短信,三个任务完成后才返回给客户端。
- 并行的方式:将注册信息写入数据库后,同时发送邮件和短信,以上三个任务完成后才返回给客户端。
在这两种方式中发送邮件或者发送短信并不是必须的,不应该阻碍到程序的正常执行流程。因此可以尝试将发送邮件和发送短信的业务交给消息队列进行异步处理。
这种方法的所用时间就比上面两种就大大减少了。
可以使用RabbitMQ中的广播模型
应用解耦
场景:用户下订单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。
这样做的问题是:当库存系统出现故障时,订单就会失败,不符合系统应该高可用的特性。因此可以在订单系统和库存系统中引入消息队列,进行订单系统和应用系统的解耦。
其工作流程大致如下:
- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
- 库存系统:订阅下单信息,获取下单消息,进行库存操作。就是库存系统出现故障,订单也会保存在消息队列中,确保消息不丢失。
流量削峰
场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
- 可以控制活动人数,超过一定阈值时直接抛弃用户请求。
- 可以缓解短时间高流量压垮应用。
PS:当然这种直接使用java代码是粗糙的,就跟我们刚开始学JDBC的连接一样,即繁琐又重复。上面一直在说RabbitMQ可以和spring天然集成,使用起来非常方便,下篇博客再记录与spring boot的整合的使用方式。