目录
-
AMPQ协议
- 为什么RabbitMQ是基于信道Channel处理而不是Connection?
-
组件和架构
- 可以存在没有exchange的队列吗?
- 核心概念
- 注意
-
图形化界面举栗
- 发布订阅模式演示
- 路由模式演示
- 主题模式演示
- 参数模式演示
-
代码举栗
- 发布订阅模式演示
- 路由模式演示
-
轮询、公平分发模式
-
使用场景
一、AMPQ协议
概述
- AMQP全称 Advanced Message Queuing Protocal(高级消息队列协议)。
- 是一个应用层协议的开发标准,为面向消息的中间件设计
生产者基本实现过程
- 创建连接工厂
- 获取连接对象
- 获取连接信道
- 创建交换机,声明队列、绑定关系、路由key、发送消息、接受信息
- 发送消息
- 关闭信道
- 关闭连接
消费者基本实现流程
- 创建连接工厂
- 获取连接对象
- 获取连接信道
- 接受信息
- 关闭信道
- 关闭连接
1.为什么RabbitMQ是基于信道Channel处理而不是Connection?
为什么RabbitMQ不是使用 http短连接的Connection 而是另外开启信道处理数据?
- 因为TCP\IP短连接需要经历3次握手、4次挥手才能通信,效率不是很高,而且连接的开关较为耗时
- 如果不用信道,那应用程序就会以TCP连接RabbitMQ,高峰时每秒成千上万条连接会造成资源巨大浪费,而且操作系统每秒处理TCP连接数也是有限制的,必定造成性能瓶颈, 一个连接内可以开启多个信道,一个信道是一个长连接,因此可以更好地处理并发数据
- 信道的原理是一条线程一条通道,多条线程多条通道同用一条TCP连接。一条TCP连接可以容纳无限的信道,即使每秒成千上万的请求也不会成为性能瓶颈
二、组件和架构
1.可以存在没有exchange的队列吗?
之前的举栗,发现生产者指定交换机为空
// 6. 发送消息到queue
/**
* 参数
*
* 交换机
* 队列、路由key
* 消息的状态控制
* 消息主题
*/
channel.basicPublish("",queueName,null,msg.getBytes());
- 不可以,虽然没有指定交换机,但是会存在一个默认的交换机
- Erlang语言是开发路由器的语言,由c实现。生产者发送的消息是给exchange的,而不是之间给队列。
- 队列存储的消息分发的时候,没有指定 路由key的情况会默认分发,反之经过key筛选,分发给指定的消费者
2.核心概念
-
Server:
又称作Broker,用于接受客户端的连接,实现AMQP实体服务 -
Connection:
连接,应用程序与Broker的网络连接。应用程序和Broker的连接TCP\IP三次挥手四次握手。 -
Channel:
网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可建立多个Channel,每个Channel代表一个会话任务 -
Message:
消息,服务器和应用程序之间传送的数据,有Properties和Body组成。Properties可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body则是消息体内容,即我们要传输的数据;
仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。需要为每一个Connection创建Channel,AMQP协议规定只有通过Channel才能执行AMQP的命令。一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与Broker交互,如果每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就算操作系统也无法承受每秒建立如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel,至少要保证共用Channel的线程发送消息必须是串行的,但是建议尽量共用Connection。 -
Virtual Host:
虚拟地址,是一个逻辑概念,用于进行逻辑隔离,是最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue,同一个Virtual Host里面不能有相同名称的Exchange或者Queue;
Virtual Host是权限控制的最小粒度;
-
Exchange:
交换机,用于接收消息,可根据路由键将消息转发到绑定的队列。不具备消息存储的能力 -
Binding:
Exchange和Queue之间的虚拟连接,Exchange在与多个Message Queue发生Binding后会生成一张路由表,路由表中存储着Message Queue所需消息的限制条件即Binding Key。
当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与Exchange Type将Message路由到Message Queue。
Binding Key由Consumer在Binding Exchange与Message Queue时指定,维持 Routing Key 和 交换机、队列之间的关系
Routing Key由Producer发送Message时指定,两者的匹配方式由Exchange Type决定。 -
Routing Key:
一个路由规则,虚拟机可用它来确定如何路由一个特定的消息;有RoutingKey的话会过滤信息,只给部分的消费者;没有的话直接发布订阅给全部的消费者一份 -
Queue:
也称作Message Queue,即消息队列,用于保存消息并将他们转发给消费者;
运行流程
3.注意
- 简单模式和工作模式有一个默认的交换机,生产者得消息都是发送给交换机,由交换机分发
- 注意图形化界面 Ack message requeue false 会模拟消费信息,导致队列的消息被消费就消失了,使用 Nack message requeue true 预览队列中的信息,不会消费。
三、图形化界面演示
1.发布订阅模式演示
创建 路由器exchange
队列绑定 路由器
队列查看消息
2.路由模式演示
路由交换机绑定队列,给每个对列指定Routing Key
指定Routing Key 为 email 发送信息测试,发现只有Q1、Q3收到信息
3.主题模式演示
- 以
.
分割表示一个字符 #
0个或多个字符匹配*
一个字符匹配
4.参数模式演示
四、代码演示
1.fanout模式
注意:
- 交换机my-fanout和 xiaosiQueue1、xiaosiQueue2、xiaosiQueue3已经在可视化界面创建并已完成绑定,不再需要创建新的队列
- 本次演示fanout,每条队列都能收到存储信息,然后模拟三条线程消费。
代码
================ Producer ====================================== Producer=====================
package henu.soft.xiaosi.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) {
// 所有的中间件技术都是基于TCP\IP协议之上构建新型的协议规范,主不过rabbitmq遵循amqp协议 ip、port
/**
* 原生方式fanout使用rabbitmq
*/
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/xiaosi");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建连接Connection
connection = factory.newConnection("生产者");
// 3. 通过连接获取管道channel
channel = connection.createChannel();
// 4. 创建交换机,声明队列、绑定关系、路由key、发送消息、接受信息
// 创建交换机,因为图形化界面已经创建过了,这里不需要再次创建
String exchangeName = "my-fanout";
String type = "fanout";
// RouteKey
String routeKey = "";
// 5. 准备消息内容
String msg = "fanout 下的 xiaosi 的 RabbitMQ~~";
// 6. 发送消息到queue
/**
* 参数
*
* fanout交换机
* 路由key
* 消息的状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, msg.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 7. 关闭通道
if (channel != null && channel.isOpen()) {
channel.close();
}
// 8. 关闭连接
if (connection != null && channel.isOpen()) {
connection.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
================ Consumer ====================================== Consumer =====================
package henu.soft.xiaosi.routing;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public void consumer(String queueName) {
/**
* fanout原生方式使用rabbitmq
*/
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/xiaosi");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建连接Connection
connection = factory.newConnection("消费者");
// 3. 通过连接获取管道channel
channel = connection.createChannel();
channel.basicConsume(
queueName,
true,
new DeliverCallback() {
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(Thread.currentThread().getName() + " 收到的信息 ===》" + new String(message.getBody(), "UTF-8"));
}
},
new CancelCallback() {
public void handle(String consumerTag) throws IOException {
System.out.println("消费者接受消息失败!");
}
});
System.out.println(Thread.currentThread().getName() + " 开始接受新消息!");
System.in.read();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 7. 关闭通道
if (channel != null && channel.isOpen()) {
channel.close();
}
// 8. 关闭连接
if (connection != null && channel.isOpen()) {
connection.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
class test {
public static void main(String[] args) {
Consumer consumer = new Consumer();
/**
* 需要注意的是 可视化界面绑定了 my-fanout 交换机和 xiaosiQueue1、xiaosiQueue2、xiaosiQueue3三个队列
*/
new Thread(() -> {
consumer.consumer("xiaosiQueue1");
}, "消费者1").start();
new Thread(() -> {
consumer.consumer("xiaosiQueue2");
}, "消费者3").start();
new Thread(() -> {
consumer.consumer("xiaosiQueue3");
}, "消费者2").start();
}
}
2.direct模式
注意
- 交换机my-direct + Route Key 和 Q1、Q2、Q3 已经在可视化界面创建并已完成绑定,不再需要创建新的队列
- 本次演示Route key 设置为 email,Q1、Q3收到存储信息,然后模拟三条线程消费,只有两条能消费。
代码
======================= Producer =======================================
package henu.soft.xiaosi.direct;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) {
// 所有的中间件技术都是基于TCP\IP协议之上构建新型的协议规范,主不过rabbitmq遵循amqp协议 ip、port
/**
* 原生方式direct使用rabbitmq
*/
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/xiaosi");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建连接Connection
connection = factory.newConnection("生产者");
// 3. 通过连接获取管道channel
channel = connection.createChannel();
// 4. 创建交换机,声明队列、绑定关系、路由key、发送消息、接受信息
// 创建交换机,因为图形化界面已经创建过了,这里不需要再次创建
String exchangeName = "my-direct";
String type = "direct";
// RouteKey
/**
* Q1===>email
* Q2====>phone
* Q3====>email、phone
*/
String routeKey = "email";
/**
* 参数:
* 队列名称
* 是否需要持久化
* 排他性
* 是否自动删除(最后一个消费者消费完是否删除队列
* 携带附加参数
*/
// 5. 准备消息内容
String msg = "direct + Route Key ======> xiaosi 的 RabbitMQ~~";
// 6. 发送消息到queue
/**
* 参数
*
* fanout交换机
* 路由key
* 消息的状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, msg.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 7. 关闭通道
if (channel != null && channel.isOpen()) {
channel.close();
}
// 8. 关闭连接
if (connection != null && channel.isOpen()) {
connection.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
======================= Consumer ============================
package henu.soft.xiaosi.direct;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public void consumer(String queueName,String threadName) {
/**
* direct原生方式使用rabbitmq
*/
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/xiaosi");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建连接Connection
connection = factory.newConnection("消费者");
// 3. 通过连接获取管道channel
channel = connection.createChannel();
channel.basicConsume(
queueName,
true,
new DeliverCallback() {
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(threadName + " 收到的信息 ===》" + new String(message.getBody(), "UTF-8"));
}
},
new CancelCallback() {
public void handle(String consumerTag) throws IOException {
System.out.println("消费者接受消息失败!");
}
});
System.out.println(Thread.currentThread().getName() + " 开始接受新消息!");
System.in.read();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 7. 关闭通道
if (channel != null && channel.isOpen()) {
channel.close();
}
// 8. 关闭连接
if (connection != null && channel.isOpen()) {
connection.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
class test {
public static void main(String[] args) {
Consumer consumer = new Consumer();
/**
* 需要注意的是 可视化界面绑定了 my-direct交换机 + Route Ksy 和 Q1、Q2、Q3三个队列
*/
new Thread(() -> {
consumer.consumer("Q1",Thread.currentThread().getName());
}, "消费者1").start();
new Thread(() -> {
consumer.consumer("Q2",Thread.currentThread().getName());
}, "消费者2").start();
new Thread(() -> {
consumer.consumer("Q3",Thread.currentThread().getName());
}, "消费者3").start();
}
}
3.代码声明交换机、队列绑定
// 4. 创建交换机,声明队列、绑定关系、路由key、发送消息、接受信息
String exchangeName = "direct_message_exchange";
String exchangeType = "direct";
// 1. 声明交换机
channel.exchangeDeclare(exchangeName,exchangeType,true);
// 2. 声明队列
String queueName1 = "xiaoxiaosi1";
String queueName2 = "xiaoxiaosi2";
String queueName3 = "xiaoxiaosi3";
/**
* 参数:
* 队列名称
* 是否需要持久化
* 排他性
* 是否自动删除(最后一个消费者消费完是否删除队列
* 携带附加参数
*/
channel.queueDeclare(queueName1,false,false,false,null);
channel.queueDeclare(queueName2,false,false,false,null);
channel.queueDeclare(queueName3,false,false,false,null);
// 3. 准备消息内容
String msg = "xiaoxiaosiRabbitMQ~~";
// 4. 绑定交换机和队列
channel.queueBind("xiaoxiaosi1",exchangeName,"xiaoxiao");
channel.queueBind("xiaoxiaosi2",exchangeName,"xiaoxiao");
channel.queueBind("xiaoxiaosi3",exchangeName,"xiaoxiaosi");
// 5. 发送消息到queue
/**
* 参数
*
* 交换机
* 队列、路由key
* 消息的状态控制
* 消息主题
*/
String routeKey = "xiaoxiao";
channel.basicPublish(exchangeName,routeKey,null,msg.getBytes());
System.out.println("消息发送成功!");
4.轮询、公平分发举栗
注意
- 默认的轮询分发模式下,不会因为服务器能力影响分发数量。应答方式可以为自动应答,但是一般真实环境都改为手动。
- 公平分发的模式下,一定要将自动应答改为手动应答,
- 公平分发模式下,需要设置
basicQos
消费者一次取多少条消息处理,需要根据服务器具体性能及相关因素确定。
五、使用场景(解耦、削峰、异步)
下订单业务场景
-
串行方式:将订单消息写入数据库成功后,发送注册邮件,在发送注册信息。以上三个任务完成后,返回给客户端
-
并行方式 异步线程池:将订单信息写入数据库之后,三个任务同时交给线程池,最后返回给客户端。可以减少处理的时间
-
异步消息队列方式:把下单作为生产者、其他三个任务作为消费者
方便解耦
流量削峰