什么是RabbitMQ
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现, 能够实现异步消息处理。RabbitMQ是支持持久化消息队列的消息中间件,应用在上下游的层次级业务逻辑中,上级业务逻辑相当于生产者发布消息,下级业务逻辑相当于消费者接受到消息并且消费消息。主流的MQ产品有很多,如ActiveMQ(基于JMS)、RabbitMQ(基于AMQP协议)、RocketMQ(基于JMS)和Kafka。
核心基础概念
Server: 又称之为Broker,接受客户端的连接,实现AMQP实体服务。
Connection: 连接,应用程序与Broker的网络连接。
Channel: 网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可以建立多个Channel,每个Channel代表一个会话任务。如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
Message: 消息,服务器和应用程序之间传送的数据,由Message Properties和Body组成。Properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性,Body就是消息体内容。
Virtual Host: 虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue,同一个Virtual Host里面不能有相同名称的Exchange或者Queue。
Exchange: 交换机,只有转发能力不具备存储消息能力,根据路由键转发消息到绑定的队列。
Binding: Exchange和Queue之间的虚拟连接,binding中可以包含routing key。
Routing key: 一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。
Queue: 也可以称之为Message Queue(消息队列),保存消息并将它们转发到消费者。
windows下安装RabbitMq
安装
直接在官网下载地址下载安装包,安装步骤略(比较简单)
管理界面
登录浏览器 http://localhost:15672 进行查看 初始化默认密码都为guest/guest
添加用户
这里我添加了用户名为admin123,密码为123456,添加完用该用户名登录
五种核心消息模型
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
</dependencies>
创建RabbitMQ连接的工具类
package com.example.mqdemo.utils;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("127.0.0.1");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/test");
factory.setUsername("admin123");
factory.setPassword("123456");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
基本消息模型
示例图
P(producer/ publisher):生产者,如寄快递
C(consumer):消费者,如收快递
红色区域:队列,如快递区,等待消费者拿快递
一句话总结
生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。
代码例子
发送方
package com.example.mqdemo.service;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明(创建)队列,必须声明队列才能够发送消息,不存在则创建。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送者发送了:" + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
查看控制台
查看管理界面
可以看到新建了一个队列,名字就是simple_queue。这里说明一下消息这列代表什么含义:
- Ready:待消费的消息总数。
- Unacked:待应答的消息总数。
- Total:总数 Ready+Unacked。
点击队列名字进入详情,点击get message,查看消息详情
消费方
package com.example.mqdemo.service;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者消费了: : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
控制台
查看管理界面
可以看到此时消息已经被消费啦。如果消费者这边程序没有停止,那么会一直在监听队列中是否有新的消息。发送方一发送到队列,消费者就立即消费
面试题1:消息确认机制
从上面例子可以看到消息一旦被消费者接收,队列中的消息就会被删除。那么如果消费者接收了消息却还没执行业务就挂掉了呢?而rabbitmq却不知道啊,那么此时这个消息就丢失掉了。因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。
这种回执ACK分两种情况:
-
自动ACK:消息一旦被接收,消费者自动发送ACK【场景;消息不太重要】
-
手动ACK:消息接收后,不会发送ACK,需要手动调用【场景:消息很重要】
演示自动ack存在的问题
发送方不做修改,消费方把代码改造一下
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者消费了: : " + msg);
//模拟出了异常
int i = 1/0;
}
发送者发送一条消息
启动消费者进行消费
此时消费者挂掉了,而消息却被消费了
演示手动ACK
修改消费者,去掉异常,把自动改成手动,第二个参数改成false
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, false, consumer);
发送方发送一条消息,消费者启动一下
查看管理界面
此时可以看到有一条未确认的消息
停止消费者程序,再查看
这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。
当我们关掉这个消费者,消息的状态再次称为Ready
修改代码
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者消费了: : " + msg);
//手动进行ack
channel.basicAck(envelope.getDeliveryTag(),false);
}
执行后查看管理界面
work消息模型
示例图
P(producer/ publisher):生产者,如寄快递
C1、C2(consumer):消费者,如收快递
红色区域:队列,如快递区,等待消费者拿快递
代码例子
发送方
跟上面发送方几乎一样,只是改了队列名字以及循环发送10条消息
package com.example.mqdemo.service;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class Send {
private final static String QUEUE_NAME = "work_queue";
public static void main(String[] argv) throws Exception {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明(创建)队列,必须声明队列才能够发送消息,不存在则创建。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循环发布消息
for (int i = 0; i < 10; i++) {
// 消息内容
String message = "快递【" + i + "】";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("卖家发送了" + message);
/*快递运输慢*/
Thread.sleep(i * 2);
}
//关闭通道和连接
channel.close();
connection.close();
}
}
消费方
消费者1
package com.example.mqdemo.service;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv1 {
private final static String QUEUE_NAME = "work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者拿到了: : " + msg);
//手动进行ack
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
消费者2
跟消费者1一样,只不过拿快递慢了些
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者拿到了: : " + msg);
try {
//模拟去拿快递慢
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
}
//手动进行ack
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
同时启动好两个消费者,然后启动发送方,结果可以看到消费者各自拿了5个快递
能者多劳
从上面可以看到,为什么消费者2拿快递拿的慢却跟消费者1还拿得一样多呢?不应该走的快拿的快递越多吗?
那怎么实现呢?其实,只需要在消费者1和2添加channel.basicQos(1)即可。这就告诉蜡笔小新他弟不要一直向消费者发送消息,而是要等待消费者的确认了前一个消息。
查看控制台
面试题2:如何避免消息堆积?
-
采用workqueue,多个消费者监听同一队列。
-
接收到消息以后,可以通过线程池,进行异步消费。
订阅模型的特点
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
订阅模型-Fanout(广播模式)
在这种订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
示例图
P:生产者,如寄快递
X: 交换机,相当于快递公司
红色区域:队列,如快递区,等待消费者拿快递
C1、C2:消费者,如收快递
代码例子
生产者
与之前不同点是声明队列改成声明交换机
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendFanout {
private final static String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] argv) throws Exception {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "广播快递";
// 发布消息到Exchange
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("卖家发送了" + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
消费者
与之前不同的是这里需要 绑定队列到交换机
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvFanout1 {
private final static String QUEUE_NAME = "fanout_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者1拿到了:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
查看控制台
订阅模型-Direct(路由模式)
在这种订阅模式中,生产者发布消息,消费者有选择性的接收消息。队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)。消息的发送方在向Exchange发送消息时,也必须指定消息的routing key
示例图
P:生产者,如寄快递
X: 交换机,相当于快递公司
红色区域:队列,如快递区,等待消费者拿快递
C1、C2:消费者,如收快递
error、info这些就是我们讲的RoutingKey
代码示例
生产者
如卖家发货发送了两个货品,A货品选择EMS快递,B货品选择了京东快递
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendFanout {
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] argv) throws Exception {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "EMS快递";
String message2 = "京东快递";
// 发布消息到Exchange,
channel.basicPublish(EXCHANGE_NAME, "EMS", null, message.getBytes());
System.out.println("卖家发送了" + message);
channel.basicPublish(EXCHANGE_NAME, "JD", null, message2.getBytes());
System.out.println("卖家发送了" + message2);
//关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
消费者1只收EMS快递和顺丰快递
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvFanout1 {
private final static String QUEUE_NAME = "direct_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,指定收EMS快递
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS");
// 绑定队列到交换机,指定收顺丰快递
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "SF");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者1拿到了:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
消费者2只拿京东快递
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvFanout2 {
private final static String QUEUE_NAME = "direct_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,指定收京东快递
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "JD");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者2拿到了:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
查看控制台
订阅模型-Topic(通配符模式)
示例图
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
代码示例
生产者
快递公司发送红色的EMS快递和蓝色的EMS快递
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class SendFanout {
private final static String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] argv) throws Exception {
// 获取连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "红色的EMS快递";
String message2 = "蓝色的EMS快递";
// 发布消息到Exchange,
channel.basicPublish(EXCHANGE_NAME, "EMS.RED", null, message.getBytes());
System.out.println("卖家发送了" + message);
channel.basicPublish(EXCHANGE_NAME, "EMS.BLUE", null, message2.getBytes());
System.out.println("卖家发送了" + message2);
//关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
消费者1只接收红色的EMS快递
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvFanout1 {
private final static String QUEUE_NAME = "topic_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,指定收到红色EMS快递
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS.RED");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者1拿到了:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
消费者2只要是EMS快递都接收
package com.example.mqdemo.demo2;
import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvFanout2 {
private final static String QUEUE_NAME = "topic_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,指定收京东快递
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS.#");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" 消费者2拿到了:" + msg);
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
查看控制台
面试题3:如何避免消息丢失?
消费者的ACK机制
除了上文面试题1的消费者的ACK机制,可以防止消费者丢失消息。但是,如果在消费者消费之前,MQ就宕机了,消息就没了,所以解决办法就是对消息进行持久化。
消息进行持久化
将消息持久化,前提是:队列、Exchange都持久化
交换机持久化
// 声明exchange,指定类型为topic,第三个参数对交换机进行持久化
channel.exchangeDeclare(EXCHANGE_NAME, "topic",true);
队列持久化
// 声明队列,第二个参数对队列进行持久化
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
消息持久化
// 发布消息到Exchange,第三个参数对消息进行持久化
channel.basicPublish(EXCHANGE_NAME, "EMS.RED", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());