消息中间件-----RabbitMQ
中间件
我国企业从20世纪80年代开始就逐渐进行信息化建设,由于方法和体系的不成熟,以及企业业务和市场需求的不断变化,一个企业可能同时运行着多个不同的业务系统,这些系统可能基于不同的操作系统、不同的数据库、异构的网络环境。现在的问题是,如何把这些信息系统结合成一个有机地协同工作的整体,真正实现企业跨平台、分布式应用。中间件便是解决之道,它用自己的复杂换取了企业应用的简单。

中间件(Middleware)是处于操作系统和应用程序之间的软件,也有人认为它应该属于操作系统中的一部分。人们在使用中间件时,往往是一组中间件集成在一起,构成一个平台(包括开发平台和运行平台),但在这组中间件中必须要有一个通信中间件,即中间件=平台+通信,这个定义也限定了只有用于分布式系统中才能称为中间件,同时还可以把它与支撑软件和实用软件区分开来。
1. 什么是MQ
MQ(message queue),从字面意思上看,本质是个队列,先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。
在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。
1.1 MQ特点
1.1.1.流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
1.1.2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

1.1.3.异步处理
有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询。或者A提供一个callback api,B执行完之后调用api通知A服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,也不用提供callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。

2. RabbitMQ
2.1.概念
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
2.2.四大核心概念
生产者
- 产生数据发送消息的程序是生产者
交换机
- 交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。
队列
- 队列是RabbitMQ内部使用的一种数据结构,尽管消息流经RabbitMQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。
消费者
- 消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

2.3 安装rabbitmq
使用Docker安装
简单点说,镜像就类似操作系统光盘介质,容器相当于通过光盘安装后的系统。通过光盘(镜像),我们能在不同机器上部署系统(容器),系统内的操作只会保留在当前的系统(容器)中,如果要升级系统,需要使用到光盘,但是可能会导致操作系统的数据丢失。
# 安装最新版的rabitmq
docker pull rabbitmq
# 启动rabbitmq
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq
# 查看运行的容器
docker ps
# 进入容器内部
docker exec -it rabbit /bin/bash
# 开启web界面管理插件
rabbitmq-plugins enable rabbitmq_management
# 添加新用户,创建账号和密码
rabbitmqctl add_user admin xxxxx
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户角色(赋予所有权限,读写)--设置角色后可以不用在赋予权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 查看当前用户和角色
rabbitmqctl list_users
# 修改账号密码
rabbitmqctl change_password admin 123456789
# 查看当前有哪些用户
rabbitmqctl list_users
# 关闭应用
rabbitmqctl stop_app
# 清除应用
rabbitmqctl reset
# 重启应用
rabbitmqctl start_app
用添加的新用户登录web界面xxxxxxxxxxx:15672

docker出现500错误

# 进入容器
docker exec -it rabbit /bin/bash
# 进入配置文件夹
cd /etc/rabbitmq/conf.d/
# 执行指令
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
# 退出容器
exit
# 重启容器
docker restart rabbit
2.4 rabbitmq入门
创建一个空项目,新建两个模块,一个代表生产者应用,一个代表消费者应用。

注:
- 使用web界面是15672端口。
- 使用java客户端远程连接是5672端口。所以需要关闭防火墙或开启这两个端口。
消息生产者
public class Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("0.0.0.0");
factory.setUsername("admin");
factory.setPassword("admin");
factory.setPort(5672);
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
try(Connection connection = factory.newConnection(); Channel channel =
connection.createChannel()) {
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message="hello world";
/**
* 发送一个消息
* 1.发送到哪个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
}
启动该服务,在web端查看
- 该消息已经被生产者生产,且已经放入rabbitmq中。

消息消费者
public class Consumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("0.0.0.0");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("等待接收消息....");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody());
System.out.println("消费者消费消息:"+message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
3. RabbitMq支持消息的模式

- 简单模式
- 对应交换机类型:默认交换机类型 direct
- 工作队列模式
- 对应交换机类型:默认交换机类型 direct
- 发布订阅模式
- 对应交换机类型:fanout
- 特点:Fanout—发布与订阅模式,是一种广播机制,需要绑定关系,它是没有路由key的模式。
- 路由模式
- 对应交换机类型:direct
- 特点:有routing-key的匹配模式
- 主题Topic模式
- 对应交换机类型:topic
- 特点:模糊的routing-key的匹配模式
- 参数模式
- 类型:headers
- 特点:参数匹配模式

默认交换机类型

3.1 发布订阅模式---fanout

通过原始的方式整合spring(后续会整合springboot,采用配置类方式。)
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
3.1.1 生产者
- 前提:在web端新建fanout_exchange交换机,和需要广播的队列建立绑定关系。和该交换机建立绑定关系的所有队列都能收到消息。
public class Producer {
public static void main(String[] args) {
//1.新建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置rabbitmq连接参数
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
//3.设置根路径
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//4.创建连接,并为该连接起一个名字
connection = factory.newConnection("生产者");
//5.根据连接创建一个信道
channel = connection.createChannel();
//6.发送消息内容--String
String message = "Hello fanout!";
//7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
String exchangeName = "fanout_exchange";
//8.设置路由key---fanout模式无路由key
String routingKey = "";
//9.发送 参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
System.out.println("消息发送成功。");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送出现异常。");
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}if (connection!=null &&connection.isOpen()){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
启动该生产者,web端查看
由于fanout_exchange只和queue1和queue3进行了绑定,所以只有这两个队列收到了广播消息。


3.1.2 消费者
- 通过实现runnable接口,重写run方法实现多线程开启。
- 前提:通过web界面创建了队列(也可以通过代码创建队列)
public class Consumer implements Runnable{
@Override
public void run() {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
//将线程名作为队列名,创建线程的时候传入队列名即可。
final String queueName = Thread.currentThread().getName();
try {
connection=factory.newConnection("消费者");
channel=connection.createChannel();
/**
* 创建队列方法-----Rabbitmq不允许创建两个相同的队列名称,否则会报错。
* 1. queue 队列的名字
* 2. durable 队列是否持久化
* 3. exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* 4. autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息
* 5. arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
*/
//channel.queueDeclare(queueName,true,false,false,null);
//接收消息
channel.basicConsume(queueName, true, new DeliverCallback() {
//接收消息成功回调函数
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(),"utf-8"));
}
}, new CancelCallback() {
//接收消息异常回调函数
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Consumer(),"queue1").start();
new Thread(new Consumer(),"queue2").start();
new Thread(new Consumer(),"queue3").start();
}
}
只有1和3线程消费了消息,因为线程1和线程3分别绑定的是队列1和队列3。

3.2 路由模式---direct
- 交换机类型:direct
- 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。

3.2.1 生产者

public class Producer {
//前提:在web端新建direct_exchange交换机,和需要广播的队列建立绑定关系和路由key。
//和该交换机建立绑定关系且符合路由key的所有队列都能收到消息。
public static void main(String[] args) {
//1.新建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置rabbitmq连接参数
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
//3.设置根路径
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//4.创建连接,并为该连接起一个名字
connection = factory.newConnection("生产者");
//5.根据连接创建一个信道
channel = connection.createChannel();
//6.发送消息内容--String
String message = "Hello direct!";
//7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
String exchangeName = "direct_exchange";
//8.设置路由key---direct模式有路由key----根据路由key,队列1和2能收到数据
String routingKey = "email";
//String routingKey2 = "sms";
//9.发送 参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
//可以根据不同的路由key发送给多个队列
//channel.basicPublish(exchangeName, routingKey2, null, message.getBytes());
System.out.println("消息发送成功。");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送出现异常。");
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}if (connection!=null &&connection.isOpen()){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

3.2.2 消费者
消费者和fanout代码相同
public class Consumer implements Runnable{
@Override
public void run() {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
//将线程名作为队列名,创建线程的时候传入队列名即可。
final String queueName = Thread.currentThread().getName();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
connection=factory.newConnection("消费者");
channel=connection.createChannel();
channel.basicConsume(queueName, true, new DeliverCallback() {
//接收消息成功回调函数
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(),"UTF-8"));
}
}, new CancelCallback() {
//接收消息异常回调函数
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Consumer(),"queue1").start();
new Thread(new Consumer(),"queue2").start();
new Thread(new Consumer(),"queue3").start();
}
}
3.3 主题模式---topic

- 类型:topic
- 特点:Topic模式是direct模式上的一种叠加,增加了模糊路由RoutingKey的模式。
3.3.1 生产者
在web端建立交换机和队列之间的绑定关系,及每个队列的模糊匹配规则。

public class Producer {
//前提:在web端新建topic_exchange交换机,和需要广播的队列建立绑定关系和路由key。
//和该交换机建立绑定关系且符合路由key的所有队列都能收到消息。
public static void main(String[] args) {
//1.新建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置rabbitmq连接参数
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
//3.设置根路径
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//4.创建连接,并为该连接起一个名字
connection = factory.newConnection("生产者");
//5.根据连接创建一个信道
channel = connection.createChannel();
//6.发送消息内容--String
String message = "Hello topic!";
//7.使用哪个交换机进行发送--该交换机需要存在,否则会报错
String exchangeName = "topic_exchange";
//8.设置路由key---topic模式有模糊路由匹配规则
String routingKey = "com.email.phone.xxx";
//9.发送 参数:1.交换机 2.路由key/队列名称 3.属性配置 4.发送内容
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
//可以根据不同的key发送给多个队列
//channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
System.out.println("消息发送成功。");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送出现异常。");
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}if (connection!=null &&connection.isOpen()){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

3.3.2 消费者
消费者和之前的一样。
public class Consumer implements Runnable{
@Override
public void run() {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
//将线程名作为队列名,创建线程的时候传入队列名即可。
final String queueName = Thread.currentThread().getName();
try {
connection=factory.newConnection("消费者");
channel=connection.createChannel();
//接收消息成功回调函数
//接收消息异常回调函数
channel.basicConsume(queueName, true,
(s, delivery) -> System.out.println(queueName+"消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8")),
s -> System.out.println("消息接收失败"));
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Consumer(),"queue1").start();
new Thread(new Consumer(),"queue2").start();
new Thread(new Consumer(),"queue3").start();
}
}
3.4 工作模式---轮询

1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;
3.4.1 轮询模式生产者
生产者将数据放入一个指定的队列,由两个消费者共同消费该队列中的数据。
public class Producer {
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("轮询生产者");
channel = connection.createChannel();
//创建一个队列queue5
channel.queueDeclare("queue5",true,false,false,null);
//发送消息,轮询使用默认交换机即可
for (int i = 0; i < 20; i++) {
String message="work: "+i;
channel.basicPublish("","queue5",null,message.getBytes());
}
System.out.println("消息发送成功。");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送出现异常。");
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}if (connection!=null &&connection.isOpen()){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
此时队列5中就存在20条数据了。

3.4.2 轮询模式消费者
消费者1
该消费者消费速度慢,通过线程休眠模拟速度。
public class Consumer1 {
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
try {
connection=factory.newConnection("消费者");
channel=connection.createChannel();
channel.basicConsume("queue5", true,
(s, delivery) -> {
System.out.println("消费者1消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},
s -> System.out.println("消息接收失败"));
System.out.println("work1开始接收消息");
//加入该代码,回调函数才执行
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者2
public class Consumer2 {
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
try {
connection=factory.newConnection("消费者");
channel=connection.createChannel();
channel.basicConsume("queue5", true,
(s, delivery) -> {
System.out.println("消费者2消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
},
s -> System.out.println("消息接收失败"));
System.out.println("work2开始接收消息");
//加上该代码,方法回调才生效。
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
测试,先启动消费者,再启动生产者
虽然消费者2比消费者1执行速度快,但总体消费数据条数还是一样的。


3.5 工作模式入门案例---公平
- 特点:由于消息接收者处理消息的能力不同,存在处理快慢的问题,我们就需要能者多劳,处理快的多处理,处理慢的少处理;
3.5.1 公平模式生产者
先启动一次生产者,在虚拟根节点下创建queue6队列。
public class Producer {
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
connection = factory.newConnection("公平生产者");
channel = connection.createChannel();
//创建一个队列
channel.queueDeclare("queue6",true,false,false,null);
//发送消息,轮询使用默认交换机即可
for (int i = 1; i <= 20; i++) {
String message="work: "+i;
channel.basicPublish("","queue6",null,message.getBytes());
}
System.out.println("消息发送成功。");
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送出现异常。");
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}if (connection!=null &&connection.isOpen()){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.5.2 公平模式消费者
- 将自动应答改为false。
- 设置同一时刻服务器发多少条消息给消费者。
channel.basicQos(1); - 在回调方法中手动应答。
消费者1
public class Consumer1 {
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
try {
connection=factory.newConnection("公平消费者1");
channel=connection.createChannel();
//1.同一时刻,服务器只会推送一条消息给消费者
channel.basicQos(1);
Channel finalChannel = channel;
//2.第二个参数改为手动应答
channel.basicConsume("queue6", false,
(s, delivery) -> {
try {
System.out.println("公平消费者1消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
Thread.sleep(2000);
//3.手动应答
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
e.printStackTrace();
}
},
s -> System.out.println("消息接收失败"));
System.out.println("work1开始接收消息");
//加入该代码,回调函数才执行
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者2
public class Consumer2 {
public static void main(String[] args) {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("8.141.52.45");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel=null;
try {
connection=factory.newConnection("公平消费者2");
channel=connection.createChannel();
channel.basicQos(1);
Channel finalChannel = channel;
channel.basicConsume("queue6", false,
(s, delivery) -> {
try {
System.out.println("公平消费者2消息接收成功,消息内容:"+new String(delivery.getBody(), "UTF-8"));
Thread.sleep(500);
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
e.printStackTrace();
}
},
s -> System.out.println("消息接收失败"));
System.out.println("work2开始接收消息");
//加上该代码,方法回调才生效。
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
//先关闭通道再关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
先启动消费者,再启动生产者。
- 消费者2处理消息量应该是消费者1的4倍左右
- 一个2000毫秒,一个500毫秒。


4. RabbitMq使用场景
4.1 同步异步的问题(串行)
- 串行方式:将订单信息写入数据库成功后,发送订单通知短信,再发送订单通知邮件。
- 需要等以上三个任务全部完成后,才能返回给客户端。(同一个线程,由上向下依次执行。)
public void makeOrder(){
// 1 :保存订单
orderService.saveOrder();
// 2: 发送短信服务
messageService.sendSMS("order");
// 3: 发送email服务
emailService.sendEmail("order");
// 4: 发送APP服务
appService.sendApp("order");
}
解决方案:采用并行方式
- 并行方式:将订单信息写入数据库成功后,发送邮件的同时,发送短信。且同时返回给客户端。
- 与串行的差别是,并行的方式可以提高处理的时间。
- 不用等待短信和邮件发送成功,即可通过开启一个新线程将订单发送给客户端。

public void makeOrder(){
// 1 :保存订单
orderService.saveOrder();
// 相关发送
relationMessage();
}
public void relationMessage(){
// 异步
theadpool.submit(new Callable<Object>{
public Object call(){
// 2: 发送短信服务
messageService.sendSMS("order");
}
})
// 异步
theadpool.submit(new Callable<Object>{
public Object call(){
// 3: 发送email服务
emailService.sendEmail("order");
}
}
// 异步
theadpool.submit(new Callable<Object>{
public Object call(){
// 4: 发送客户端服务
appService.sendApp("order");
}
})
}
存在问题:
1:耦合度高
2:需要自己写线程池自己维护成本太高
3:出现了消息可能会丢失,需要你自己做消息补偿
4:如何保证消息的可靠性你自己写
5:如果服务器承载不了,你需要自己去写高可用
采用异步消息队列的方式

好处
1:完全解耦,用MQ建立桥接
2:有独立的线程池和运行模型
3:MQ有持久化功能
4:死信队列和消息转移的等
5:HA镜像模型高可用。
按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。
public void makeOrder(){
// 1 :保存订单
orderService.saveOrder();
rabbitTemplate.convertSend("ex","2","消息内容");
}
4.2 解决高耦合问题
用户下单成功后,需要向其再添加推送微信消息。
- 如果是之前异步处理,需要修改代码。
- 如果采用异步消息队列的方式,只需要在微信服务中从队列获取数据即可。

4.3 流量削峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
5. SpringBoot整合RabbitMQ

5.1 SpringBoot整合RabbitMQ-----fanout模式

- 发布订阅模式
- 对应交换机类型:fanout
- 特点:Fanout—发布与订阅模式,是一种广播机制,需要绑定关系,它是没有路由key的模式。
5.1.1 生产者
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置文件
# 服务端口
server:
port: 8083
# 配置rabbitmq服务
spring:
rabbitmq:
username: admin
password: 123
virtual-host: /
host: 8.141.52.45
port: 5672
- 创建service
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void makeOrderFanout(Long userId, Long productId, int num) {
// 1: 模拟用户下单
String orderNumer = UUID.randomUUID().toString();
// 2: 根据商品id productId 去查询商品的库存
// int numstore = productSerivce.getProductNum(productId);
// 3:判断库存是否充足
// if(num > numstore ){ return "商品库存不足..."; }
// 4: 下单逻辑
// orderService.saveOrder(order);
// 5: 下单成功要扣减库存
// 6: 下单完成以后
System.out.println("fanout用户 " + userId + ",订单编号是:" + orderNumer);
// 发送订单信息给RabbitMQ fanout_order_exchange交换机,交换机不存在的话就创建
rabbitTemplate.convertAndSend("fanout_order_exchange", "", orderNumer);
}
}
- 配置类
- 要保证beanName唯一
import org.springframework.amqp.core.*;
@Configuration
public class FanoutRabbitConfig {
//创建Fanout交换机
@Bean
public FanoutExchange fanoutOrderExchange() {
return new FanoutExchange("fanout_order_exchange", true, false);
}
//创建三个队列
@Bean
public Queue emailFanoutQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
return new Queue("email.fanout.queue", true,false,false);
}
@Bean
public Queue smsFanoutQueue() {
return new Queue("sms.fanout.queue", true,false,false);
}
@Bean
public Queue weixinFanoutQueue() {
return new Queue("weixin.fanout.queue", true,false,false);
}
//建立绑定关系
@Bean
public Binding bindingDirect1() {
return BindingBuilder.bind(weixinFanoutQueue()).to(fanoutOrderExchange());
}
@Bean
public Binding bindingDirect2() {
return BindingBuilder.bind(smsFanoutQueue()).to(fanoutOrderExchange());
}
@Bean
public Binding bindingDirect3() {
return BindingBuilder.bind(emailFanoutQueue()).to(fanoutOrderExchange());
}
- controller
@RestController
public class MyController {
@Autowired
OrderService orderService;
@RequestMapping("/order/fanout")
public void makeOrder() throws Exception {
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
Long userId = 100L + i;
Long productId = 10001L + i;
int num = 10;
orderService.makeOrderFanout(userId, productId, num);
}
}
}
此时访问/order请求,就会生成交换机,队列。及绑定关系以及在队列中添加10条数据。


5.1.2 消费者
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
service
@RabbitListener(queues = "email.fanout.queue") //通过注解绑定队列
@Component
public class EmailService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("email-------------->" + message);
}
}
-------------------------------------------------------------------
@RabbitListener(queues = "sms.fanout.queue") //通过注解绑定队列
@Component
public class SmsService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("sms-------------->" + message);
}
}
-------------------------------------------------------------------
@RabbitListener(queues = "weixin.fanout.queue") //通过注解绑定队列
@Component
public class WeiXinService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("weixin-------------->" + message);
}
}
由于service是一个组件,在springboot启动的时间就加载
- 所以当消息队列中存在数据时,消费者项目一启动就进行消费。


5.2 SpringBoot整合RabbitMQ-----direct模式
- 交换机类型:direct
- 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。
5.2.1 生产者
配置类
- bean名要唯一
- 建立绑定关系时通过with()方法创建路由key。
@Configuration
public class DirectRabbitConfig {
//创建direct交换机
@Bean
public DirectExchange directOrderExchange() {
return new DirectExchange("direct_order_exchange", true, false);
}
//创建三个队列
@Bean
public Queue emailDirectQueue() {
return new Queue("email.direct.queue", true,false,false);
}
@Bean
public Queue smsDirectQueue() {
return new Queue("sms.direct.queue", true,false,false);
}
@Bean
public Queue weixinDirectQueue() {
return new Queue("weixin.direct.queue", true,false,false);
}
//建立绑定关系
@Bean
public Binding bindingDirect4() {
return BindingBuilder.bind(weixinDirectQueue()).to(directOrderExchange()).with("weixin");
}
@Bean
public Binding bindingDirect5() {
return BindingBuilder.bind(smsDirectQueue()).to(directOrderExchange()).with("sms");
}
@Bean
public Binding bindingDirect6() {
return BindingBuilder.bind(emailDirectQueue()).to(directOrderExchange()).with("email");
}
}
Service
- 在业务逻辑侧确定将消息发给哪些符合路由key的队列。
@Override
public void makeOrderDirect(Long userId, Long productId, int num) {
String orderNumer = UUID.randomUUID().toString();
System.out.println("direct用户 " + userId + ",订单编号是:" + orderNumer);
//发微信和邮件给对应的队列
rabbitTemplate.convertAndSend("direct_order_exchange", "email", orderNumer);
rabbitTemplate.convertAndSend("direct_order_exchange", "weixin", orderNumer);
}
Controller
- 发送请求,
email和weixin两个队列会受到数据。
@RequestMapping("/order/direct")
public void makeOrderDirect() throws Exception {
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
Long userId = 100L + i;
Long productId = 10001L + i;
int num = 10;
orderService.makeOrderDirect(userId, productId, num);
}
}


5.2.2 消费者
- 只需要修改上方的队列名即可。
@RabbitListener(queues = "email.direct.queue") //通过注解绑定队列
@Component
public class EmailService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("email-------------->" + message);
}
}
-------------------------------------------------------------------
@RabbitListener(queues = "sms.direct.queue") //通过注解绑定队列
@Component
public class SmsService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("sms-------------->" + message);
}
}
-------------------------------------------------------------------
@RabbitListener(queues = "weixin.direct.queue") //通过注解绑定队列
@Component
public class WeiXinService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("weixin-------------->" + message);
}
}
只有email和微信能收到数据且消费数据。

5.3 SpringBoot整合RabbitMQ-----topic模式
- 对应交换机类型:topic
- 特点:模糊的routing-key的匹配模式
交换机、队列、路由key的定义及其绑定关系可以在消费者方通过注解的方式实现。
5.3.1 生产者
Service
@Override
public void makeOrderTopic(Long userId, Long productId, int num) {
String orderNumer = UUID.randomUUID().toString();
System.out.println("topic用户 " + userId + ",订单编号是:" + orderNumer);
//发微信和邮件给对应的队列
rabbitTemplate.convertAndSend("topic_order_exchange", "com.email.weixin.xxx", orderNumer);
}
Controller
@RequestMapping("/order/topic")
public void makeOrderTopic() throws Exception {
for (int i = 1; i <= 10; i++) {
Thread.sleep(1000);
Long userId = 100L + i;
Long productId = 10001L + i;
int num = 10;
orderService.makeOrderTopic(userId, productId, num);
}
}
通过注解在消费者方定义交换机和队列,先不要启动生产者,因为此时交换机不存在。
5.3.2 消费者
Service
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "email.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
key = "*.email.#"
)) //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class EmailTopicService {
@RabbitHandler //代表此方法是一个消息接收的方法。不要有返回值
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("emailTopic-------------->" + message);
}
}
----------------------------------------------------------------------------------------------------
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "sms.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
key = "#.sms.#"
)) //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class SmsTopicService {
// @RabbitHandler 代表此方法是一个消息接收的方法。不要有返回值
@RabbitHandler
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("sms-------------->" + message);
}
}
----------------------------------------------------------------------------------------------------
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "weixin.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
key = "#.weixin.*"
)) //通过注解定义交换机、队列、路由key及其绑定关系
@Component
public class WeiXinTopicService {
// @RabbitHandler 代表此方法是一个消息接收的方法。不要有返回值
@RabbitHandler
public void messageReceive(String message){
// 此处省略发邮件的逻辑
System.out.println("weixin-------------->" + message);
}
}
通过注解进行交换机和队列之间的绑定。

根据生产者的路由模糊匹配规则
- email和weixin符合条件。这两个队列将收到数据。
//发微信和邮件给对应的队列
rabbitTemplate.convertAndSend("topic_order_exchange", "com.email.weixin.xxx", orderNumer);
生产订单数据。

消费订单数据。

建议
- 建议使用配置类来管理交换机和队列之间的关系,方便管理和维护。
- 之后的死信队列、延时队列需要。
配置类在消费者方进行配置(创建交换机、队列及其绑定关系)。- 先启动消费者服务,再启动生产者服务。
6. RabbitMQ高级用法---过期时间TTL
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。
RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
- 第二种方法是对消息进行单独设置,每条消息TTL可以不同。(不建议使用)
- 如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。
消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。
应用场景
- 如果用户下单超过20分钟未付款,将该订单传入死信队列。(不会真正删除)。
6.1 设置队列过期时间
配置类
@Configuration
public class TTLRabbitConfig {
//创建direct交换机
@Bean
public DirectExchange directTTLExchange() {
return new DirectExchange("ttl_direct_exchange", true, false);
}
//创建一个队列
@Bean
public Queue ttlDirectQueue() {
//添加过期时间5秒,将参数传入
HashMap<String, Object> map = new HashMap<>();
map.put("x-message-ttl",5000);
return new Queue("ttl.direct.queue", true,false,false,map);
}
//建立绑定关系
@Bean
public Binding bindingDirect7() {
return BindingBuilder.bind(ttlDirectQueue()).to(directTTLExchange()).with("ttl");
}
}
启动项目,访问请求,10条数据生产完毕。

5秒之后,未有消费者进行消费,数据自动删除。

6.2 设置消息过期时间
- 新建队列时不用传入参数,发送数据时传入设置项。
@Override
public void makeTTL(Long userId, Long productId, int num) {
String orderNumer = UUID.randomUUID().toString();
System.out.println("ttl用户 " + userId + ",订单编号是:" + orderNumer);
MessagePostProcessor processor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
//发送数据时传入设置项。
rabbitTemplate.convertAndSend("ttl_direct_exchange", "ttl", orderNumer,processor);
}
7. RabbitMQ高级用法---死信队列
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange 指定交换机即可。

7.1 配置死信交换机和死信队列
如果在线上碰到一个需要修改配置的交换机或队列,不要删除,新建一个新的交换机或队列和之前的产生关系。
@Configuration
public class TTLRabbitConfig {
//创建direct交换机
@Bean
public DirectExchange directTTLExchange() {
return new DirectExchange("ttl_direct_exchange", true, false);
}
//创建死信交换机
@Bean
public DirectExchange directDeadExchange() {
return new DirectExchange("ttl_dead_exchange", true, false);
}
//创建死信队列,数据过期后会被放到死信队列中
@Bean
public Queue ttlDeadQueue(){
return new Queue("ttl.dead.queue",true,false,false);
}
//死信交换机和死信队列进行绑定
@Bean
public Binding bindingDirect8() {
return BindingBuilder.bind(ttlDeadQueue()).to(directDeadExchange()).with("dead");
}
//创建一个队列
@Bean
public Queue ttlDirectQueue() {
//添加过期时间5秒
HashMap<String, Object> map = new HashMap<>();
map.put("x-message-ttl",5000);
map.put("x-dead-letter-exchange","ttl_dead_exchange");
//direct模式需要设置key
map.put("x-dead-letter-routing-key","dead");
return new Queue("ttl.direct.queue", true,false,false,map);
}
//建立绑定关系
@Bean
public Binding bindingDirect7() {
return BindingBuilder.bind(ttlDirectQueue()).to(directTTLExchange()).with("ttl");
}
}

8. RabbitMQ高级用法---内存磁盘和监控


浙公网安备 33010602011771号