1.RabbitMQ概念
1.1.基本概念
MQ全称为Message Queue,即消息队列. 它也是一个队列,遵循FIFO原则.
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue Protocol高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开 发中应用非常广泛。
1.2.RabbitMQ使用场景
-
消峰&限流
-
应用解耦
-
异步处理
-
数据分发
-
消息通讯
-
分布式事务
-
排序保证&先进先出
场景原理图
1.3.常见的消息队列
ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。
2.RabbitMQ的原理
2.1.原理图
2.2.执行流程
组成部分说明如下:
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
消息发布接收流程:
-----发送消息-----
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
----接收消息-----
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
3.安装RabbitMQ
3.1.安装erlang语音
otp_win64_20.2.exe
3.2.配置erlang的环境变量
3.3.安装RabbitMQ
rabbitmq-server-3.7.4.exe
3.4.安装RabbitMQ的可视化插件
进入mq地 安装目录的sbin目录,cmd执行 rabbitmq-plugins.bat enable rabbitmq_management
3.5.访问测试
http://localhost:15672 账号默认: guest/guest
4.Rabbit的使用-helloworld
4.1.helloworld结构图
4.2.helloworld完成
4.2.1.导入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<!--和springboot2.0.5对应-->
<version>5.4.1</version>
</dependency>
4.2.2.准备连接工具类
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
//Virtual代表虚拟消息服务器,每个服务器相对独立
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
4.2.3.创建消息发送者
public class Sen {
//队列的名字
private static final String QUEUE_HELLO = "queue_hello";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try{
//1.创建链接对象
connection = ConnectionUtil.getConnection();
//2.创建通道
channel = connection.createChannel();
//3.声明队列:队列名字 ; 持久 ;独占链接 ;不用即删除 ; 参数
channel.queueDeclare(QUEUE_HELLO, true, false, false, null);
//4.发送消息:交换机 ; 队列名(routingkey) ;参数 ;消息了内容
channel.basicPublish("", QUEUE_HELLO ,null ,"我是一个hello消息".getBytes() );
System.out.println("已经发送消息:我是一个hello消息");
} catch (Exception e) {
e.printStackTrace();
}finally {
if(channel != null){
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
4.2.4.创建消息接受者
public class Rev {
//队列的名字
private static final String QUEUE_HELLO = "queue_hello";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(QUEUE_HELLO, true, false, false, null);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(QUEUE_HELLO, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2.5.手动签收
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(QUEUE_HELLO, false,consumer);
在回调方法中
channel.basicAck(deliveryTag, false);
5.RabbitMq-workqueues
5.1.workqueues的思想
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
5.2.workqueues代码实现
5.2.1.创建消息发送者
同 helloword一样,只是改个队列的名字
5.2.2.创建多个消息接受者
同 helloword一样,只是改个队列的名字
5.2.3.设置能者多劳
设置接受者同时只能处理一个消息
channel.basicQos(1);
6.发布订阅模型
6.1.原理图
解读:
1、1个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
6.2.Exchange分类
-
Fanout:广播
将消息交给所有绑定到交换机的队列 all
-
Direct:定向
把消息交给符合指定routing key 的队列 一堆或一个
-
Topic:通配符
把消息交给符合routing pattern(路由模式)的队列 一堆或者一个
7.Fanout广播模式
7.1.Fanout广播模式
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
7.2.Fanout模式的原理
在广播模式下,消息发送流程是这样的:
- 1) 可以有多个消费者
- 2) 每个消费者有自己的queue(队列)
- 3) 每个队列都要绑定到Exchange(交换机)
- 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 5) 交换机把消息发送给绑定过的所有队列
- 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
7.3.实现Fanout模式
发送者需要声明交换机 ,不需要声明队列,发送消息的时候需要指定交换机,不需要指定routingkey接受者需要声明队列 , 需要给队列绑定交换机 ,接受者的交换机和消息发送者的交换机要一致。多个消息接受者,声明的队列的名字需要不一样,而交换机的名字需要一样。
7.3.1.消息发送者
public class Sen {
//交换机的名字
private static final String EXCHANGE_FANOUT = "exchange_fanout";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try{
//1.创建链接对象
connection = ConnectionUtil.getConnection();
//2.创建通道
channel = connection.createChannel();
//3.声明交换机指定类型为fanout
channel.exchangeDeclare(EXCHANGE_FANOUT, BuiltinExchangeType.FANOUT);
//4.发送消息:交换机 ; 队列名(routingkey) ;参数 ;消息了内容
String message = "我是一个fanout消息";
channel.basicPublish(EXCHANGE_FANOUT, "" ,null ,message.getBytes() );
System.out.println("已经发送消息:"+message);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(channel != null){
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
7.3.2.消息接收者1
public class Rev {
//队列的名字
private static final String FANOUT_QUEUE_1 = "fanout_queue_1";
//交换机的名字
private static final String EXCHANGE_FANOUT = "exchange_fanout";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(FANOUT_QUEUE_1, false, false, false, null);
//4.队列绑定交换机
channel.queueBind(FANOUT_QUEUE_1, EXCHANGE_FANOUT, "");
//设置同时处理一个消息
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
//System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
//System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(FANOUT_QUEUE_1, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
7.3.3.消息接收者2
public class Rev2 {
//队列的名字
private static final String FANOUT_QUEUE_2 = "fanout_queue_2";
//交换机的名字
private static final String EXCHANGE_FANOUT = "exchange_fanout";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(FANOUT_QUEUE_2, false, false, false, null);
//4.队列绑定交换机
channel.queueBind(FANOUT_QUEUE_2, EXCHANGE_FANOUT, "");
//设置同时处理一个消息
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
//System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
//System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(FANOUT_QUEUE_2, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
8.direct定向模式
direct定向模式是交换机根据product指定的routingkey,收到消息后去匹配指定routingkey的队列,将消息发送给队列,消费者进行消费
8.1.工作模式
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
8.2.direct实现
8.2.1.发送者
public class Sen {
//交换机的名字
private static final String EXCHANGE_DIRECT = "exchange_direct";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try{
//1.创建链接对象
connection = ConnectionUtil.getConnection();
//2.创建通道
channel = connection.createChannel();
//3.声明交换机指定类型为DIRECT
channel.exchangeDeclare(EXCHANGE_DIRECT, BuiltinExchangeType.DIRECT);
//4.发送消息:交换机 ; 队列名(routingkey) ;参数 ;消息了内容
String message = "我是一个DIRECT消息";
//使用 定向的交换机 ,往 rotingkey为 update的队列中发送消息
channel.basicPublish(EXCHANGE_DIRECT, "delete" ,null ,message.getBytes() );
System.out.println("已经发送消息:"+message);
...
8.2.2.接收者
public class Rev {
//队列的名字
private static final String DIRECT_QUEUE_1 = "direct_queue_1";
//交换机的名字
private static final String EXCHANGE_DIRECT = "exchange_direct";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(DIRECT_QUEUE_1, false, false, false, null);
//4.队列绑定交换机
channel.queueBind(DIRECT_QUEUE_1, EXCHANGE_DIRECT, "update");
channel.queueBind(DIRECT_QUEUE_1, EXCHANGE_DIRECT, "delete");
//设置同时处理一个消息
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
//System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
//System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(DIRECT_QUEUE_1, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:多个消息接收者,只需要声明不同的队列即可 , 交换机是相同的 ,并且绑定队列的时候要指定队列的routingkey
9.topic通配符
topic的工作模式跟 direct是一样的 , direct定向模式是交换机根据product指定的routingkey,收到消息后去匹配指定routingkey的队列,将消息发送给队列,消费者进行消费 , 只是不一样的是,消费者在绑定队列的时候队列的routingkey可以使用通配符
9.1.topic工作模式
# 匹配多个
* 匹配一个
9.1.topic代码实现
9.1.1.发送者
public class Sen {
//交换机的名字
private static final String EXCHANGE_TOPIC = "exchange_topic";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try{
//1.创建链接对象
connection = ConnectionUtil.getConnection();
//2.创建通道
channel = connection.createChannel();
//3.声明交换机指定类型为TOPIC
channel.exchangeDeclare(EXCHANGE_TOPIC, BuiltinExchangeType.TOPIC);
//4.发送消息:交换机 ; 队列名(routingkey) ;参数 ;消息了内容
String message = "我是一个TOPIC消息";
//使用 定向的交换机 ,往 rotingkey为 update的队列中发送消息
channel.basicPublish(EXCHANGE_TOPIC, "user.update" ,null ,message.getBytes() );
System.out.println("已经发送消息:"+message);
...
9.1.2.接收者
public class Rev2 {
//队列的名字
private static final String TOPIC_QUEUE_2 = "topic_queue_2";
//交换机的名字
private static final String EXCHANGE_TOPIC = "exchange_topic";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(TOPIC_QUEUE_2, false, false, false, null);
//4.队列绑定交换机 , routingkey使用通配符
channel.queueBind(TOPIC_QUEUE_2, EXCHANGE_TOPIC, "user.*");
//设置同时处理一个消息
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
//System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
//System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(TOPIC_QUEUE_2, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:不是说一样就非得使用通配符
10.header模式
10.1.工作模式
header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配 队列。
案例: 根据用户的通知设置去通知用户,设置接收Email的用户只接收Email,设置接收sms的用户只接收sms,设置两种 通知类型都接收的则两种通知都有效。
10.2.实现
10.2.1发送者
public class Sen {
//交换机的名字
public static final String EXCHANGE_HEADER = "exchange_header";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try{
//1.创建链接对象
connection = ConnectionUtil.getConnection();
//2.创建通道
channel = connection.createChannel();
//3.声明交换机指定类型为TOPIC
channel.exchangeDeclare(EXCHANGE_HEADER, BuiltinExchangeType.HEADERS);
//4.发送消息:交换机 ; 队列名(routingkey) ;参数 ;消息了内容
String message = "我是一个header消息";
//使用 定向的交换机 ,往 rotingkey为 update的队列中发送消息
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type_email", "email");//匹配email通知消费者绑定的header
headers.put("inform_type_sms", "sms");//匹配email通知消费者绑定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
channel.basicPublish(EXCHANGE_HEADER, "" ,properties.build() ,message.getBytes() );
System.out.println("已经发送消息:"+message);
10.2.2接收者
public class Rev {
//队列的名字
private static final String HEADER_QUEUE_1 = "header_queue_1";
public static void main(String[] args) {
try {
//1.创建链接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare(HEADER_QUEUE_1, false, false, false, null);
//4.队列绑定交换机
Map<String, Object> headersEmail = new Hashtable<String, Object>();
headersEmail.put("inform_type_email", "email");
channel.queueBind(HEADER_QUEUE_1, Sen.EXCHANGE_HEADER, "",headersEmail);
//设置同时处理一个消息
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)throws IOException {
//交换机
String exchange = envelope.getExchange();
//System.out.println("exchange:"+exchange);
//路由key
String routingKey = envelope.getRoutingKey();
//System.out.println("routingKey:"+routingKey);
//消息id
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
//消息内容
String msg = new String(body, "utf8");
System.out.println("收到消息:" + msg);
//System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
}
};
//4.接受消息 :队列 ; 签收方式(true:自动签收)
channel.basicConsume(HEADER_QUEUE_1, false,consumer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
11.持久化
11.1交换机持久化
11.2.队列持久化
11.3.消息持久化
12.如何避免消息丢失?
12.1 .手动签收
消费者的ACK机制。可以防止消费者丢失消息。
12.2 .持久化
持久化可以防止消费者丢失消息。