写在前面:
该文章创作于2021/11/25,大部分内容来自于学相伴飞哥RabbitMQ笔记,原文章链接:https://www.kuangstudy.com/zl/rabbitmq#1366722371607715841,请访问该网址获取更多内容!!!
一、中间件概述
中间件(Middleware)是处于操作系统和应用程序之间的软件,也有人认为它应该属于操作系统中的一部分。人们在使用中间件时,往往是一组中间件集成在一起,构成一个平台(包括开发平台和运行平台),但在这组中间件中必须要有一个通信中间件,即中间件=平台+通信,这个定义也限定了只有用于分布式系统中才能称为中间件,同时还可以把它与支撑软件和实用软件区分开来。
01、消息中间件的特点?
也许很难给中间件一个严格的定义,但中间件应具有如下的一些特点:
(1)满足大量应用的需要
(2)运行于多种硬件和OS平台
(3)支持分布计算,提供跨网络、硬件和OS平台的透明性的应用或服务的交互
(4)支持标准的协议
(5)支持标准的接口
02、什么时候使用中间件?
在项目的架构和重构中,使用任何技术和架构的改变我们都需要谨慎斟酌和思考,因为任何技术的融入和变化都可能人员,技术,和成本的增加,中间件的技术一般现在一些互联网公司或者项目中使用比较多,如果你仅仅还只是一个初创公司建议还是使用单体架构,最多加个缓存中间件即可,不要盲目追求新或者所谓的高性能,而追求的背后一定是业务的驱动和项目的驱动,因为一旦追求就意味着你的学习成本,公司的人员结构以及服务器成本,维护和运维的成本都会增加,所以需要谨慎选择和考虑。
03、使用消息中间件的好处?
1:跨系统数据传递
2:高并发的流量削峰
3:数据的分发和异步处理
4:大数据分析与传递
5:分布式事务
比如你有一个数据要进行迁移或者请求并发过多的时候,比如你有10W的并发请求下订单,我们可以在这些订单入库之前,我们可以把订单请求堆积到消息队列中,让它稳健可靠的入库和执行。
04、常见的消息中间件?
ActiveMQ、RabbitMQ、Kafka、RocketMQ等。
消息中间件是一种接受数据,接受请求、存储数据、发送数据等功能的技术服务。
05、消息中间件的核心?
1:消息的协议
2:消息的持久化机制
3:消息的分发策略
4:消息的高可用,高可靠
5:消息的容错机制
06、消息中间件使用的协议?
消息中间件采用的并不是http协议,而常见的消息中间件协议有:OpenWire、AMQP、MQTT、Kafka,OpenMessage协议。
AMQP协议
AMQP:(全称:Advanced Message Queuing Protocol) 是高级消息队列协议。由摩根大通集团联合其他公司共同设计。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
特性:
1:分布式事务支持。
2:消息的持久化支持。
3:高性能和高可靠的消息处理优势。
MQTT协议
MQTT协议:(Message Queueing Telemetry Transport)消息队列是IBM开放的一个即时通讯协议,物联网系统架构中的重要组成部分。支持者:RabbitMQ
特点:
1:轻量
2:结构简单
3:传输快,不支持事务
4:没有持久化设计。
应用场景:
1:适用于计算能力有限
2:低带宽
3:网络不稳定的场景。
OpenMessage协议
是近几年由阿里、雅虎和滴滴出行、Stremalio等公司共同参与创立的分布式消息中间件、流处理等领域的应用开发标准。支持者:RocketMQ
特点:
1:结构简单
2:解析速度快
3:支持事务和持久化设计。
Kafka协议
Kafka协议是基于TCP/IP的二进制协议。消息内部是通过长度来分割,由一些基本数据类型组成。支持者:kafka。
特点是:
1:结构简单
2:解析速度快
3:无事务支持
4:有持久化设计
面试题:为什么消息中间件不直接使用http协议呢?
1: 因为http请求报文头和响应报文头是比较复杂的,包含了cookie,数据的加密解密,状态码,响应码等附加的功能,但是对于一个消息而言,我们并不需要这么复杂,也没有这个必要性,它其实就是负责数据传递,存储,分发就行,一定要追求的是高性能。尽量简洁,快速。
2:大部分情况下http大部分都是短链接,在实际的交互过程中,一个请求到响应很有可能会中断,中断以后就不会就行持久化,就会造成请求的丢失。这样就不利于消息中间件的业务场景,因为消息中间件可能是一个长期的获取消息的过程,出现问题和故障要对数据或消息就行持久化等,目的是为了保证消息和数据的高可靠和稳健的运行。
07、消息队列持久化?
简单来说就是将数据存入磁盘,而不是存在内存中随服务器重启断开而消失,使数据能够永久保存。
常见的持久化方式:
RabbitMQ | Kafka | RocketMQ | |
---|---|---|---|
文件存储 | 支持 | 支持 | 支持 |
数据库存储 | / | / | / |
08、消息分发策略?
MQ消息队列有如下几个角色
1:生产者
2:存储消息
3:消费者
那么生产者生成消息以后,MQ进行存储,消费者是如何获取消息的呢?一般获取数据的方式无外乎推(push)或者拉(pull)两种方式,典型的git就有推拉机制,我们发送的http请求就是一种典型的拉取数据库数据返回的过程。而消息队列MQ是一种推送的过程,而这些推机制会适用到很多的业务场景也有很多对应推机制策略。
消息分发机制:
RabbitMQ | Kafka | RocketMQ | |
---|---|---|---|
发布订阅 | 支持 | 支持 | 支持 |
轮询分发 | 支持 | 支持 | / |
公平分发 | 支持 | 支持 | / |
重发 | 支持 | / | 支持 |
消息拉取 | 支持 | 支持 | 支持 |
09、消息的高可用?
所谓高可用:是指产品在规定的条件和规定的时刻或时间内处于可执行规定功能状态的能力。
当业务量增加时,请求也过大,一台消息中间件服务器的会触及硬件(CPU,内存,磁盘)的极限,一台消息服务器你已经无法满足业务的需求,所以消息中间件必须支持集群部署。来达到高可用的目的。
反正终归三句话:
1:要么消息共享,
2:要么消息同步
3:要么元数据共享
参考文章:https://www.kuangstudy.com/zl/rabbitmq#1366048193061646338
二、RabbitMQ安装
RabbitMQ是一个开源的遵循AMQP协议实现的基于Erlang语言编写,支持多种客户端(语言)。用于在分布式系统中存储消息,转发消息,具有高可用,高可扩性,易用性等特征。
官网:https://www.rabbitmq.com/
1:下载地址:https://www.rabbitmq.com/download.html
2:环境准备:CentOS7.x+ / Erlang
RabbitMQ是采用Erlang语言开发的,所以系统环境必须提供Erlang环境,第一步就是安装Erlang。
01、开放相关端口
5672:RabbitMQ的通讯端口
25672:RabbitMQ的节点间的CLI通讯端口是
15672:RabbitMQ HTTP_API的端口,管理员用户才能访问,用于管理RabbitMQ,需要启动Management插件。
1883,8883:MQTT插件启动时的端口。
61613、61614:STOMP客户端插件启用的时候的端口。
15674、15675:基于webscoket的STOMP端口和MOTT端口
注意: 直接安装RabbitMQ比较麻烦,所以采用docker安装方式
02、使用docker安装
前提:需安装docker,docker相关的命令
# 启动docker:
systemctl start docker
# 停止docker:
systemctl stop docker
# 重启docker:
systemctl restart docker
# 查看docker状态:
systemctl status docker
# 开机启动:
systemctl enable docker
systemctl unenable docker
# 查看docker概要信息
docker info
# 查看docker帮助文档
docker --help
参考网站:
1:https://www.rabbitmq.com/download.html
2:https://registry.hub.docker.com/_/rabbitmq/
01、获取镜像
docker pull rabbitmq:management
02、运行容器
docker run -di --name=myrabbit -p 15672:15672 rabbitmq:management
—hostname:指定容器主机名称
—name:指定容器名称
-p:将mq端口号映射到本地
或者运行时设置用户和密码
docker run -di --name myrabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management
查看日志:
docker logs -f myrabbit
03、访问
使用 http://你的IP地址:15672
访问rabbit控制台
03、额外的Linux相关排查命令
more xxx.log 查看日记信息
netstat -naop | grep 5672 查看端口是否被占用
ps -ef | grep 5672 查看进程
systemctl stop 服务
三、RabbitMQ角色分类
1:none:
- 不能访问management plugin
2:management:查看自己相关节点信息
- 列出自己可以通过AMQP登入的虚拟机
- 查看自己的虚拟机节点 virtual hosts的queues,exchanges和bindings信息
- 查看和关闭自己的channels和connections
- 查看有关自己的虚拟机节点virtual hosts的统计信息。包括其他用户在这个节点virtual hosts中的活动信息。
3:Policymaker
- 包含management所有权限
- 查看和创建和删除自己的virtual hosts所属的policies和parameters信息。
4:Monitoring
- 包含management所有权限
- 罗列出所有的virtual hosts,包括不能登录的virtual hosts。
- 查看其他用户的connections和channels信息
- 查看节点级别的数据如clustering和memory使用情况
- 查看所有的virtual hosts的全局统计信息。
5:Administrator
- 最高权限
- 可以创建和删除virtual hosts
- 可以查看,创建和删除users
- 查看创建permisssions
- 关闭所有用户的connections
四、RabbitMQ核心组成部分
核心概念:
Server:又称Broker ,接受客户端的连接,实现AMQP实体服务。 安装rabbitmq-server
Connection:连接,应用程序与Broker的网络连接 TCP/IP/ 三次握手和四次挥手
Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立对各Channel,每个Channel代表一个会话任务。
Message :消息:服务与应用程序之间传送的数据,由Properties和body组成,Properties可是对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则就是消息体的内容。
Virtual Host 虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机理由可以有若干个Exhange和Queueu,同一个虚拟主机里面不能有相同名字的Exchange
Exchange:交换机,接受消息,根据路由键发送消息到绑定的队列。(不具备消息存储的能力)
Bindings:Exchange和Queue之间的虚拟连接,binding中可以保护多个routing key.
Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。
Queue:队列:也成为Message Queue,消息队列,保存消息并将它们转发给消费者。
01、RabbitMQ的运行流程
rabbitmq发送消息一定有一个交换机
参考视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=15&spm_id_from=pageDriver
五、RabbitMQ入门案例
01、Simple模式
图解
1:构建一个maven工程
2:导入rabbitmq的maven依赖
java原生依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
spring依赖
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-amqp</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
springboot依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
番外:rabbitmq和spring同属一个公司开放的产品,所以他们的支持也是非常完善,这也是为什么推荐使用rabbitmq的一个原因。
3:启动rabbitmq-server服务
systemctl start rabbitmq-server
或者
docker start myrabbit
4:定义生产者
package qd.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 简单队列生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 4:通过创建交换机,,绑定关系,路由key,发送消息,接收消息
String queueName = "queue1"; // 声明队列
/**
* 声明队列名称
* 是否持久化 所谓持久化是否存盘,非持久化也会存盘,但是会随着重启服务而丢失
* 排他性,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* 是否自动删除,最后一个消费者完毕消息是否把队列自动删除
* 携带附属参数 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
*/
channel.queueDeclare(queueName, false, false, false, null);
// 5:准备消息内容
String message = "Hello,qiandu!";
// 6:发送消息给队列
/**
* 交换机
* 队列
* 消息得状态控制
* 消息主题
*/
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
1:执行发送,这个时候可以在web控制台查看到这个队列queue的信息
2:我们可以进行对队列的消息进行预览和测试如下:
3:进行预览和获取消息进行测试
5:定义消费者
package qd.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 简单模式消费者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 4:开始消费消息
channel.basicConsume("queue1", true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("收到得消息是" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
02、Fanout模式
图解
- 特点:Fanout—发布与订阅模式,是一种广播机制,它是没有路由key的模式。
生产者
队列已经提前绑定好了
package qd.fanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* fanout模式生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 5:准备消息内容
String message = "Hello,qiandu!";
// 6:准备交换机
String exchangeName = "fanout-exchange";
// 7:定义路由 fanout模式无需指定路由
String routeKey = "";
// 8:指定交换机的类型
String type = "fanout";
// 9:发送消息给队列
/**
* 交换机
* 路由
* 消息得状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException 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 (Exception e) {
e.printStackTrace();
}
}
}
}
}
消费者
package qd.fanout;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* fanout模式消费者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer {
public static Runnable runnable = () -> {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
//获取队列名称
String qunueName = Thread.currentThread().getName();
// 4:开始消费消息
channel.basicConsume(qunueName, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("收到得消息是" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
public static void main(String[] args) {
new Thread(runnable, "qunune-1").start();
new Thread(runnable, "qunune-2").start();
new Thread(runnable, "qunune-3").start();
}
}
03、Direct模式
图解
- 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。
生产者
队列、路由已经提前绑定好了
package qd.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;
/**
* Direct模式生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 5:准备消息内容
String message = "Hello,qiandu!";
// 6:准备交换机
String exchangeName = "direct-exchange";
// 7:定义路由
String routeKey = "sms";
String routeKey1 = "email";
// 8:指定交换机的类型
String type = "direct";
// 9:发送消息给队列
/**
* 交换机
* 路由
* 消息得状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException 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 (Exception e) {
e.printStackTrace();
}
}
}
}
}
消费者
package qd.direct;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Direct模式消费者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer {
public static Runnable runnable = () -> {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
//获取队列名称
String qunueName = Thread.currentThread().getName();
// 4:开始消费消息
channel.basicConsume(qunueName, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("收到得消息是" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
public static void main(String[] args) {
new Thread(runnable, "queue-1").start();
new Thread(runnable, "queue-2").start();
}
}
04、Topic模式
图解
- 特点:Topic模式是direct模式上的一种叠加,增加了模糊路由RoutingKey的模式。
生产者
队列、路由已经提前绑定好了
package qd.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* topic模式生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 5:准备消息内容
String message = "Hello,qiandu!";
// 6:准备交换机
String exchangeName = "topic-exchange";
// 7:定义路由 # 匹配多级 * 匹配1级
String routeKey = "com.user.order";
String routeKey1 = "com.order";
// 8:指定交换机的类型
String type = "topic";
// 9:发送消息给队列
/**
* 交换机
* 路由
* 消息得状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException 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 (Exception e) {
e.printStackTrace();
}
}
}
}
}
消费者
package qd.topic;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* topic模式消费者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer {
public static Runnable runnable = () -> {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
//获取队列名称
String qunueName = Thread.currentThread().getName();
// 4:开始消费消息
channel.basicConsume(qunueName, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println(qunueName+"--收到得消息是" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
public static void main(String[] args) {
new Thread(runnable, "queue-1").start();
new Thread(runnable, "queue-2").start();
}
}
05、完整的创建方式
在前面的案例中,队列、路由已经提前绑定好了。这里我们编写一个完整的创建方式,注意要规范命名 如topic_message_exchange
在生产者或消费者声明都可以的。
package qd.all;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 完整案例生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 5:准备消息内容
String message = "Hello,qiandu!";
// 6:准备交换机
String exchangeName = "topic_message_exchange";
// 7:定义路由 # 匹配多级 * 匹配1级
String routeKey = "com.user.order";
String routeKey1 = "com.order";
// 8:指定交换机的类型 direct/topic/fanout/headers
String exchangeType = "topic";
/**
* 9:声明交换机
* exchangeName:交换机名称
* exchangeType:交换机类型
* 是否持久化:true
*/
channel.exchangeDeclare(exchangeName, exchangeType, true);
/**
* 10:声明队列
* 队列名
* 是否持久化
* 是否有排他性
* 是否删除
* 参数
*/
channel.queueDeclare("queue1", true, false, false, null);
/**
* 11:绑定队列与交换机的关系
* 队列
* 交换机
* 路由
*/
channel.queueBind("queue1", exchangeName, "com.#");
// 12:发送消息给队列
/**
* 交换机
* 路由
* 消息得状态控制
* 消息主题
*/
channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException 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 (Exception e) {
e.printStackTrace();
}
}
}
}
}
在实际的开发中,声明交换机、声明队列、绑定队列与交换机的关系可以通过web面板提前绑定好
06、Work模式-轮询模式
图解
当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?
主要有两种模式:
1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;
轮询模式特点:该模式接收消息是当有多个消费者接入时,消息的分配模式是一个消费者分配一条,直至消息消费完成
队列已经在web页面添加过
生产者
package qd.work.lunxun;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* work-轮询模式生产者
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Producer {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
for (int i = 0; i < 20; i++) {
// 5:准备消息内容
String message = "Hello,qiandu!" + i;
// 6:发送消息给队列
/**
* 交换机
* 路由/队列名称
* 消息得状态控制
* 消息主题
*/
channel.basicPublish("", "queue1", null, message.getBytes());
}
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException 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 (Exception e) {
e.printStackTrace();
}
}
}
}
}
消费者1 (消费者2也是同样代码)
package qd.work.lunxun;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* work-轮询模式消费者1
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer1 {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 同一时刻,服务器只会推送一条消息给消费者
//channel.basicQos(1);
// 定义接收消息的回调
Channel finalChannel = channel;
// 4:开始消费消息
// 第二个参数是否自动应答。实际生产环境中,一般为false手动应答
finalChannel.basicConsume("queue1", true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("worker2收到消息" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试结果为消费者1与2接收消息数一样
07、Work模式-公平分发
图解
当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?
主要有两种模式:
1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;
轮询模式特点:由于消息接收者处理消息的能力不同,存在处理快慢的问题,我们就需要能者多劳,处理快的多处理,处理慢的少处理
生产者
与轮询分发模式生产者相同
消费者1 (消费者2也是同样代码)
主要改变:改为手动应答,加入finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
package qd.work.fair;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* work-公平分发模式消费者1
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/24 23:46
*/
public class Consumer1 {
public static void main(String[] args) {
// 1:创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("114.115.167.190");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2:设置连接属性
connection = connectionFactory.newConnection("生产者");
// 3:获取连接通道
channel = connection.createChannel();
// 同一时刻,服务器只会推送一条消息给消费者 根据硬件情况设置
channel.basicQos(1);
// 定义接收消息的回调
Channel finalChannel = channel;
// 4:开始消费消息
// 第二个参数是否自动应答。实际生产环境中,一般为false手动应答
finalChannel.basicConsume("queue1", false, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("worker2收到消息" + new String(delivery.getBody(), "UTF-8"));
// 改为手动应答
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("消息接收失败");
}
}
);
System.out.println("开始接收消息");
System.in.read();// 阻断
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
// 7:关闭连接
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 8:关闭通道
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
六、RabbitMQ使用场景
01、解耦、削峰、异步
01-1、同步异步问题(串行)
串行方式:将订单信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端
public void makeOrder(){
// 1 :保存订单
orderService.saveOrder();
// 2: 发送短信服务
messageService.sendSMS("order");//1-2 s
// 3: 发送email服务
emailService.sendEmail("order");//1-2 s
// 4: 发送APP服务
appService.sendApp("order");
}
01-2、并行方式 异步线程池
并行方式:将订单信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
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");
}
})
}
存在问题:
1:耦合度高
2:需要自己写线程池自己维护成本太高
3:出现了消息可能会丢失,需要你自己做消息补偿
4:如何保证消息的可靠性你自己写
5:如果服务器承载不了,你需要自己去写高可用
01-3、异步消息队列的方式
public void makeOrder(){
// 1 :保存订单
orderService.saveOrder();
rabbitTemplate.convertSend("ex","2","消息内容");
}
好处
1:完全解耦,用MQ建立桥接
2:有独立的线程池和运行模型
3:出现了消息可能会丢失,MQ有持久化功能
4:如何保证消息的可靠性,死信队列和消息转移的等
5:如果服务器承载不了,你需要自己去写高可用,HA镜像模型高可用。
按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍
02、高内聚,低耦合
异步、解耦
03、流量削锋
04、分布式事务的可靠消费和可靠生产
05、索引、缓存、静态化处理的数据同步
06、流量监控
07、日志监控(ELK)
08、下单、订单分发、抢票
七、RabbitMQ-SpringBoot案例
01、总体步骤
1、创建SPringBoot工程
2、引入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3、在application.yml
进行配置
server:
port: 8080
## RabbitMQ配置
spring:
rabbitmq:
addresses: 114.115.167.190
port: 2567
username: admin
password: admin
virtual-host: /
02、fanout模式
生产者
定义生产者
package com.qd.service.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* Create by IntelliJ IDEA
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/25 8:28
*/
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1:定义交换机
private String exchangeName = "fanout_order_exchange";
// 2:定义路由
private String routeKey = "";
public void makeOrder(Long userId, Long productId) {
// 模拟用户下单
String orderNum = UUID.randomUUID().toString();
System.out.println("用户" + userId + "的订单编号为" + orderNum);
// 发送订单信息给RabbitMQ
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNum);
}
}
配置类进行绑定关系等
package com.qd.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Create by IntelliJ IDEA
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/25 8:28
*/
@Configuration
public class FanoutRabbitMQConfig {
// 配置队列
@Bean
public Queue emailQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
return new Queue("email.fanout.queue", true);
}
@Bean
public Queue smsQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
return new Queue("sms.fanout.queue", true);
}
// 交换机
@Bean
public FanoutExchange fanoutDirectExchange() {
return new FanoutExchange("fanout_order_exchange", true, false);
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingEmail() {
return BindingBuilder.bind(emailQueue()).to(fanoutDirectExchange());
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingSms() {
return BindingBuilder.bind(smsQueue()).to(fanoutDirectExchange());
}
}
测试类测试
@SpringBootTest
class RabbitMqProducerApplicationTests {
@Autowired
private OrderService orderService;
// 测试 发布消息
@Test
void contextLoads() {
orderService.makeOrder(1L, 4L);
}
}
消费者
消费者–短信服务
/**
* Create by IntelliJ IDEA
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/25 9:42
*/
@Component
@RabbitListener(queues = {"sms.fanout.queue"})
public class FanoutSmsConsumer {
@RabbitHandler
public void reviceMssage(String message) {
System.out.println("sms.fanout----接到消息" + message);
}
}
消费者–邮件服务
/**
* Create by IntelliJ IDEA
*
* @Blog: https://www.cnblogs.com/qd666
* @Author: qiandu
* @Date: 2021/11/25 9:42
*/
@Component
@RabbitListener(queues = {"email.fanout.queue"})
public class FanoutEmailConsumer {
@RabbitHandler
public void reviceMssage(String message) {
System.out.println("email.fanout----接到消息" + message);
}
}
03、direct模式
direct模式与fanout模式差不多,只需指定路由即可,不再展示
04、topic模式
生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1:定义交换机
private String exchangeName = "topic_order_exchange";
// 2:定义路由
private String routeKey = "com.sms.test";
public void makeOrder(Long userId, Long productId) {
// 模拟用户下单
String orderNum = UUID.randomUUID().toString();
System.out.println("用户" + userId + "的订单编号为" + orderNum);
// 发送订单信息给RabbitMQ
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNum);
}
}
消费者
消费者–邮件服务
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "email.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
key = "#.email.#"
))
public class TopicEmailConsumer {
@RabbitHandler
public void reviceMssage(String message) {
System.out.println("email.fanout----接到消息" + message);
}
}
消费者–短信服务
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "sms.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange",type = ExchangeTypes.TOPIC),
key = "#.sms.#"
))
public class TopicSmsConsumer {
@RabbitHandler
public void reviceMssage(String message) {
System.out.println("sms.fanout----接到消息" + message);
}
}
注意:虽然使用注解看起来很方便,不过为了业务的扩展还是推荐使用配置类方式!
八、RabbitMQ高级-过期时间TTL
01、概述
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置。
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
- 第二种方法是对消息进行单独设置,每条消息TTL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。
02、设置队列TTL
生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1:定义交换机
private String exchangeName = "ttl_fanout_exchange";
// 2:定义路由
private String routeKey = "";
public void makeOrder(Long userId, Long productId) {
// 模拟用户下单
String orderNum = UUID.randomUUID().toString();
System.out.println("用户" + userId + "的订单编号为" + orderNum);
// 发送订单信息给RabbitMQ
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNum);
}
}
配置类
@Configuration
public class TTLRabbitMQConfig {
// 声明交换机fanout模式交换机
@Bean
public FanoutExchange ttlFanoutExchange() {
return new FanoutExchange("ttl_fanout_exchange", true, false);
}
// 设置队列与过期时间
@Bean
public Queue ttlQueue() {
// 设置过期时间 5秒自动消失
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
return new Queue("ttl.fanout.queue", true, false, false, args);
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingTTL() {
return BindingBuilder.bind(ttlQueue()).to(ttlFanoutExchange());
}
}
测试
@SpringBootTest
class RabbitMqProducerApplicationTests {
@Autowired
private OrderService orderService;
// 测试 发布消息
@Test
void contextLoads() {
orderService.makeOrder(1L, 4L);
}
}
消费者
@Component
@RabbitListener(queues = {"ttl.fanout.queue"})
public class TopicSmsConsumer {
@RabbitHandler
public void reviceMssage(String message) {
System.out.println("sms.fanout----接到消息" + message);
}
}
参数 x-message-ttl 的值 必须是非负 32 位整数 (0 <= n <= 2^32-1) ,以毫秒为单位表示 TTL 的值。这样,值 6000 表示存在于 队列 中的当前 消息 将最多只存活 6 秒钟。
03、设置消息TTL
消息的过期时间;只需要在发送消息(可以发送到任何队列,不管该队列是否属于某个交换机)的时候设置过期时间即可。在测试类中编写如下方法发送消息并设置过期时间到队列
生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1:定义交换机
private String exchangeName = "ttl_fanout_exchange";
// 2:定义路由
private String routeKey = "";
public void makeOrder(Long userId, Long productId) {
// 模拟用户下单
String orderNum = UUID.randomUUID().toString();
System.out.println("用户" + userId + "的订单编号为" + orderNum);
// 给消息设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("4000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
// 发送订单信息给RabbitMQ
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNum,messagePostProcessor);
}
}
配置类
@Configuration
public class TTLRabbitMQConfig {
// 声明交换机fanout模式交换机
@Bean
public FanoutExchange ttlFanoutExchange() {
return new FanoutExchange("ttl_fanout_exchange", true, false);
}
// 设置队列与过期时间
@Bean
public Queue ttlQueue() {
// 设置过期时间 5秒自动消失
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
return new Queue("ttl.fanout.queue", true, false, false, args);
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingTTL() {
return BindingBuilder.bind(ttlQueue()).to(ttlFanoutExchange());
}
//=============================
// 设置队列
@Bean
public Queue smsQueue() {
return new Queue("ttl.sms.queue", true);
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingSms() {
return BindingBuilder.bind(smsQueue()).to(ttlFanoutExchange());
}
}
expiration 字段以微秒为单位表示 TTL 值。且与 x-message-ttl 具有相同的约束条件。因为 expiration 字段必须为字符串类型,broker 将只会接受以字符串形式表达的数字。
当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用。
九、RabbitMQ高级-消息确认机制
NONE值是禁用发布确认模式,是默认值
CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
服务配置
# 服务端口
server:
port: 8080
# 配置rabbitmq服务
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 114.115.167.190
port: 5672
publisher-confirm-type: correlated
回调
public class MessageConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息确认成功!!!!");
}else{
System.out.println("消息确认失败!!!!");
}
}
}
生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1:定义交换机
private String exchangeName = "ttl_fanout_exchange";
// 2:定义路由
private String routeKey = "";
public void makeOrder(Long userId, Long productId) {
// 模拟用户下单
String orderNum = UUID.randomUUID().toString();
System.out.println("用户" + userId + "的订单编号为" + orderNum);
// 给消息设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("4000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
// 设置消息确认机制
rabbitTemplate.setConfirmCallback(new MessageConfirmCallback());
// 发送订单信息给RabbitMQ
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNum,messagePostProcessor);
}
}
十、RabbitMQ高级-死信队列
### 01、概述
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange
指定交换机即可。
02、死信队列测试
死信队列配置
@Configuration
public class DeadRabbitMQConfig {
// 声明死信交换机
@Bean
public FanoutExchange deadFanoutExchange() {
return new FanoutExchange("dead_fanout_exchange", true, false);
}
// 设置死信队列
@Bean
public Queue deadQueue() {
return new Queue("dead.fanout.queue", true, false, false);
}
// 绑定 将队列和交换机绑定, 并设置用于匹配键
@Bean
public Binding bindingDead() {
return BindingBuilder.bind(deadQueue()).to(deadFanoutExchange());
}
}
正常消息队列配置
@Bean
public Queue ttlQueue() {
// 设置过期时间 5秒自动消失
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
args.put("x-dead-letter-exchange", "dead_fanout_exchange"); //绑定死信交换机
//args.put("x-dead-letter-routing-key", ""); // 绑定路由key fanout不需要配置
return new Queue("ttl.fanout.queue", true, false, false, args);
}
应用场景:
1:当订单未支付,可把消息放入死信队列中
2:分布式事务处理
3:…
十一、RabbitMQ运维-内存磁盘监控
01、RabbitMQ的内存警告
当内存使用超过配置的阈值或者磁盘空间剩余空间对于配置的阈值时,RabbitMQ会暂时阻塞客户端的连接,并且停止接收从客户端发来的消息,以此避免服务器的崩溃,客户端与服务端的心态检测机制也会失效。
如下图:
02、RabbitMQ的内存控制
02-1、命令的方式
rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute 50MB
02-2、配置文件方式 rabbitmq.conf
当前配置文件:/etc/rabbitmq/rabbitmq.conf
#默认
#vm_memory_high_watermark.relative = 0.4
# 使用relative相对值进行设置fraction,建议取值在04~0.7之间,不建议超过0.7.
vm_memory_high_watermark.relative = 0.6
# 使用absolute的绝对值的方式,但是是KB,MB,GB对应的命令如下
vm_memory_high_watermark.absolute = 2GB
docker方式
docker exec -t -i cluster_rabbit2_1 vi /etc/rabbitmq/rabbitmq.config
参考文章:https://www.kuangstudy.com/zl/rabbitmq#1366722371607715841
03、RabbitMQ的内存换页
在某个Broker节点及内存阻塞生产者之前,它会尝试将队列中的消息换页到磁盘以释放内存空间,持久化和非持久化的消息都会写入磁盘中,其中持久化的消息本身就在磁盘中有一个副本,所以在转移的过程中持久化的消息会先从内存中清除掉。
可以通过设置 vm_memory_high_watermark_paging_ratio
来进行调整
vm_memory_high_watermark.relative = 0.4
vm_memory_high_watermark_paging_ratio = 0.7(设置小于1的值)
04、RabbitMQ的磁盘预警
默认情况下:磁盘预警为50MB的时候会进行预警。表示当前磁盘空间第50MB的时候会阻塞生产者并且停止内存消息换页到磁盘的过程。 这个阈值可以减小,但是不能完全的消除因磁盘耗尽而导致崩溃的可能性。比如在两次磁盘空间的检查空隙内,第一次检查是:60MB ,第二检查可能就是1MB,就会出现警告。
通过命令方式修改如下:
rabbitmqctl set_disk_free_limit <disk_limit>
rabbitmqctl set_disk_free_limit memory_limit <fraction>
disk_limit:固定单位 KB MB GB
fraction :是相对阈值,建议范围在:1.0~2.0之间。(相对于内存)
通过配置文件配置如下:
disk_free_limit.relative = 3.0
disk_free_limit.absolute = 50mb
实验代码:https://chenyu6666.lanzoui.com/iBUXPwwg79g
该文章创作于2021/11/25,大部分内容来自于学相伴飞哥RabbitMQ笔记,原文章链接:https://www.kuangstudy.com/zl/rabbitmq#1366722371607715841,请访问该网址获取更多内容!!!