什么是中间件
一个企业可能同时运行着多个不同的业务系统,这些系统可能基于不同的操作系统、不同的数据库、异构的网络环境。如何把这些信息系统结合成一个有机地协同工作的整体,真正实现企业跨平台、分布式应用。中间件便是解决之道,它用自己的复杂换取了企业应用的简单。
中间件(Middleware)是处于操作系统和应用程序之间的软件,也有人认为它应该属于操作系统中的一部分。人们在使用中间件时,往往是一组中间件集成在一起,构成一个平台(包括开发平台和运行平台),但在这组中间件中必须要有一个通信中间件,即中间件+平台+通信,这个定义也限定了只有用于分布式系统中才能称为中间件,同时还可以把它与支撑软件和使用软件区分开来
为什么需要使用消息中间件
具体地说,中间件屏蔽了底层操作系统的复杂性,使程序开发人员面对一个简单而统一的开发环境,减少程序设计的复杂性,将注意力集中在自己的业务上,不必再为程序在不同系统软件上的移植而重复工作,从而大大减少了技术上的负担,中间件带给应用系统的,不只是开发的简便、开发周期的缩短,也减少了系统的维护、运行和管理的工作量,还减少了计算机总体费用的投入。
中间件特点
必须遵循一定的规范,具有高可用、高可扩、持久性扥特征,
屏蔽底层操作系统复杂性、屏蔽技术架构局限性(不需要应用程序都要使用同一个语言)
TCP/IP协议是中间的主要通信协议,但他比较底层并不能完全满足需求,我们还要基于TCP/IP构建自己的请求信息。
在项目中什么时候使用中间件技术
在项目的架构和重构中,使用任何技术和架构的改变我们都需要谨慎斟酌和思考,因为任何技术的融入和变化都可能人员,技术,和成本的增加,如果你仅仅还只是一个初创公司建议还是使用单体架构,最多加个缓存中间件即可,不要盲目追求新或者所谓的高性能,而追求的背后一定是业务的驱动和项目的驱动,因为一旦追求就意味着你的学习成本,公司的人员结构以及服务器成本,维护和运维的成本都会增加,所以需要谨慎选择和考虑。
但是作为一个开发人员,一定要有学习中间件技术的能力和思维。
为什么消息中间件还有自己的协议?
因为TCP/IP协议比较底层,无法完全满足我们的需求,所以在TCP/IP协议之上构建了自己的协议,底层还是TCP/IP
负载均衡中间:
缓存中间件:
MemCache:适合小规模缓存使用
Redis:适合大规模缓存使用
数据库中间件:
学习中间件的方式和技巧
单体架构
在企业开发当中,大部分的初期架构都采用的是单体架构的模式进行架构,而这种架构的典型的特点:就是把所有的业务和模块,源代码,静态资源文件等都放在一个工程中,如果其中的一个模块升级或者迭代发生一个很小的变动都会重新编译和重新部署项目。这种架构存在的问题是:
这样就有后续的分布式架构系统。如下
分布式架构
何谓分布式系统:
通俗一点:就是一个请求由服务器端的多个服务(服务或者系统)协同处理完成
和单体架构不同的是,单体架构是一个请求发起 jvm调度线程(确切的是 tomcat线程池)分配线程 Thread来处理请求直到释放,而分布式系统是:一个请求时由多个系统共同来协同完成,jvm和环境都可能是独立。如果生活中的比喻的话,单体架构就像建设一个小房子很快就能够搞定,如果你要建设一个鸟巢或者大型的建筑,你就必须是各个环节的协同和分布,这样目的也是项目发展到后期的时候要去部署和思考的问题。我们也不难看出来:分布式架构系统存在的特点和问题如下:
存在问题:
好处:
从上图中可以看出来,消息中间件的是
消息中间件应用的场景
比如你有10 W的并发请求下订单,我们可以在这些订单入库之前,我们可以把订单请求堆积到消息队列中,让它稳健可靠的入库和执行
串行的总时间为所有系统运行的时间之和;
并行的运行的总时间取决于最慢的那个系统的运行时间,比串行的求和要快很多;
常见的消息中间件
ActiveMQ、RabbitMQ、Kafka、RocketMQ等
消息中间件的本质及设计
它是一种接受数据、接受请求、存储数据、发送数据等功能的技术服务
MQ消息队列:负责数据的接受,存储和传递,所以性能要高于普通服务和技术
消息中间件的核心组成部分
小结
其实不论选择单体架构还是分布式架构都是项目开发的一个阶段,在什么阶段选择合适的架构方式,而不能盲目追求,最后造成的后果和问题都需要自己买单。但作为一个开发人员学习和探讨新的技术使我们每个程序开发者都应该去保持和思考的问题。当我们没办法去改变社会和世界的时候,我们为了生活和生存那就必须要迎合企业和市场的需求,发挥你的价值和所学的才能,创造价值和实现自我
什么是协议
所谓协议是指:
网络协议的三要素
比如我 MQ发送一个信息,是以什么数据格式发送到队列中,然后每个部分的含义是什么,发送完毕以后的执行的动作,以及消费者消费消息的动作,消费完毕的相应结构和反馈是什么,然后按照对应的执行顺序进行处理。如果你还是不理解:大家每天都在接触的 http请求协议:
而消息中间件采用的并不是 http协议,常见的消息中间件协议基于TCP/IP协议之上封装成:OpenWire、AMQP、MQTT、Kafka,OpenMessage协议
面试题:为什么消息中间件不直接使用 http协议
AMQP协议
AMQP:(全称:Advanced Message Queuing Protocol)是高级消息队列协议。由摩根大通集团联合其他公司共同设计。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现由 RabbitMQ等
特性:
MQTT协议
MQTT协议(Message Queueing Telemetry Transport)消息队列是 IBM开放的及时通讯协议,物联网系统架构中的重要组成部分
特点:
应用场景:
OpenMessage协议
是近几年由阿里、雅虎和滴滴出行、Stremalio等公司共同参与创立的分布式信息中间件、流处理等领域的应用开发标准
特点:
Kafka协议
Kafka协议是基于 TCP/IP的二进制协议。消息内部是 通过长度来分割,由一些基本数据类型组成
特点:
小结
协议:
其实就是 TCP/IP 协议基础之上构建的一种约定俗称的规范和机制、它的主要目的可以让客户端(应用程序 java,go)进行沟通和通讯。并且这种写一下规范必须具有持久性,高可用,高可靠的性能
持久化
简单来说就是将数据存入磁盘,而不是存在内存中随服务器重启断开而消失,使数据能够永久保存
常见的持久化方式
消息的分发策略
MQ消息 队列有如下几个角色
那么生产者生成消息以后,MQ进行存储,消费者是如何获取消息的呢?
一般获取数据的方式无外乎推(push)或者拉(pull)两种方式,典型的 git就有推拉机制,我们发送的 http请求就是一种典型的拉取数据库数据返回的过程。
而消息队列 MQ是一种推送的过程,而这些推机制会使用到很多的业务场景也有很多对应推机制策略
场景分析一
比如我在 APP上下了一个订单,我们的系统和服务很多,我们如何得知这个消息被哪个系统或者哪些服务器或者系统进行消费,那这个时候就需要一个分发的策略。这就需要消费策略。或者称之为消费的方法论
场景分析二
在发送消息的过程中可能会出现异常,或者网络的抖动,故障等等因为造成消息的无法消费,比如用户在下订单,消费 MQ接受,订单系统出现故障,导致用户支付失败,那么这个时候就需要消息中间件就必须支持消息重试机制策略。也就是支持:出现问题和故障的情况下,消息不丢失还可以进行重发
消息分发策略的机制和对比
轮询分发侧重公平性,不会因为消费处理的快与慢而对数据分发有倾斜,对应自动应答,自动ACK
公平分发侧重能者多劳,会根据实际的使用情况有一定的倾斜,对应手动应答,手动ACK
重发为了保证消息的可靠性
消息队列很少使用消息拉取操作
综合来看,Rabbit的功能最完善,最稳定;kafka性能最高,
什么是高可用机制
所谓高可用:是指产品在规定的条件和规定的时刻或时间内处于可执行规定功能状态的能力
当业务量增加时,请求也过大,一台消息中间件服务器的会触及硬件(CPU,内存,磁盘)的极限,一台消息服务器你已经无法满足业务的需求,所以消息中间件必须支持集群部署,来达到高可用的目的
说白了,就是尽可能做到,有服务器出故障、宕机之后系统也能正常使用
集群模式1 - Master-slave主从共享数据的部署方式
解释:这种模式写入消息同样在 Master主节点上,但是主节点会同步数据到 slave节点形成副本,和 zookeeper或者 redis主从机制很雷同。这样可以达到负载均衡的效果,如果消费者有多个这样就可以去不同的节点进行消费,以为消息的拷贝和同步会占用很大的带宽和网络资源。在后续的 rabbitmq中会有使用
所以要尽量部署在同一个机房内,保证带宽不受影响,
集群模式3 - 多主集群同步部署模式
解释:和上面的区别不是特别的大,但是它的写入可以往任意节点去写入
集群模式4 - 多主集群转发部署模式
解释:如果你插入的数据是 broker-1中国,元数据信息会存储数据的相关描述和记录存放的位置(队列)。它会对描述信息也就是元数据信息进行同步,如果消费者在 broker-2中进行消费,发现自己节点没有对应的信息,可以从对应的元数据信息中去查询,然后返回对应的消息信息,场景:比如买火车票或者黄牛买演唱会门票,比如第一个黄牛有顾客说要买的演唱会门票,但是没有但是他回去联系其他的黄牛询问,如果有就返回
集群模式5 Master-slave与 Broker-cluster组合的方案
解释:实现多主多从的热备机制来完成消息的高可用以及数据的热备机制,在生产规模达到一定的阶段的时候,这种使用的频率比较高
什么是高可靠机制
所谓高可靠是指:系统可以无故障低持续运行,比如一个系统突然崩溃,报错,异常等等并不影响线上业务的正常运行,出错的几率极低,就称之为:高可靠
在高并发的业务场景中,如果不能保证系统的高可靠,那造成的隐患和损失是非常严重的
如何保证中间件消息的可靠性呢,可以从两个方面考虑:
简单概述:
RabbitMQ是一个开源的遵循 AMQP协议实现的基于 Erlang语言编写,支持多种客户端(语言),用于在分布式系统中存储消息,转发消息,具有高可用,高可扩性,易用性等特征
我使用的虚拟机是Linux centos 7,因此找到对应的版本下载,这里下载的是rmp安装包,
RabbitMQ是采用 Erlang语言开发的,erlang语言是基于开发交换机的语言,性能高
所以系统环境必须提供 Erlang环境,第一步就是安装 Erlang
查看系统版本号,rabbitmq 对 erlang 有版本要求,不能使用太旧的erlang版本
https://www.rabbitmq.com/which-erlang.html
比如,rabbitmq 的最新版为 3.8.14,他要求 erlang 的最小版本为 22.3
这里我们将 rabbitmq 和 erlang 安装包提前准备好
安装包:
创建一个目录
mkdir -p /usr/rabbitmq
将 rabbitmq 和 erlang 包上传到这个目录中
解压 erlang 语言包
rpm -Uvh erlang-solutions-2.0-1.noarch.rpm
解压完毕,开始安装 erlang
yum install -y erlang
安装需要等待一点时间,安装完毕后,检测erlang版本,出现版本号表示安装成
erl -v
rabbitmq 在安装过程中需要依赖这个插件,需要先安装
yum install -y socat
解压 rabbitmq 安装包,注意实际的包名要以我们自己下载的包名
rpm -Uvh rabbitmq-server-3.8.13-1.el8.noarch.rpm
解压完毕,开始安装
yum install rabbitmq-server -y
安装完毕,启动服务
# 启动服务
systemctl start rabbitmq-server
# 查看服务状态,running表示启动成功
systemctl status rabbitmq-server.service
# 开机自启动
systemctl enable rabbitmq-server
# 停止服务
systemctl stop rabbitmq-server
默认情况下,是没有安装web端的客户端插件,需要安装才可以生效
执行命令,开始安装rabbitmq管理界面插件
rabbitmq-plugins enable rabbitmq_management
安装完毕以后,重启服务
systemctl restart rabbitmq-server
访问浏览器,访问地址:服务器 IP+端口号(默认15672)
注意:
15672
端口(rabbitmq默认端口号),5672端口后续程序需要使用也要开放guest
默认情况只能在 localhost本计下访问,所以需要添加一个远程登录的用户新增用户,账号 admin,密码 admin
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
用户操作权限分四种级别:
为用户添加资源权限(授予访问虚拟机根节点的所有资源,如果已经选择了admin,那么这个命令可以不执行)
rabbitmqctl set_permissions -p / admin ".*"".*"".*"
# 添加账号、密码
rabbitmqctl add_user
# 设置账号为管理员
rabbitmqctl set_user_tags 账号 administrator
# 修改账号密码
rabbitmqctl change_password Username Newpassword
# 查看用户清单
rabbitmqctl list_users
# 添加账号查看资源的权限
rabbitmqctl set_permissions -p / 用户名 ".*"".*"".*"
首先要在服务器中准备一个 Docker 的环境
虚拟化容器技术 - Docker的安装
# yum 包更新到最新
yum update
# 安装docker依赖的组件,yum-utils提供yum-config-manager功能,另外两个是devicemapper驱动依赖
yum install -y yum-utils device-mapper-persistent-data lvm2
# 设置yum源为阿里云
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 安装docker
yum install docker-ce -y
# 安装完毕后,检查docker版本
docker -v
# 给docker配置阿里云镜像加速器(可以不安装,看个人情况)
sudo mkdir -p /erc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry mirrors": ["https://0wrdwnn6.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
docker的相关命令
# 启动docker
systemctl start docker
# 停止docker
systemctl stop docker
# 重启docker
systemctl restart docker
# 查看docker状态
systemctl status docker
# 查看当前有哪些镜像
docker images
# 开机自启动
systemctl enable docker
systemctl unenable docker
# 查看docker概要信息
docker info
# 查看docker帮助文档
docker --help
安装 rabbitmq 方法一
# 获取rabbit镜像
docker pull rabbitmq:management
# 创建并运行容器
docker run -id --name=myrabbit -p 15672:15672 rabbitmq:management
# 参数含义
--hostname:指定容器主机名称
--name:指定容器名称
-p:将mq端口号映射到本地
安装完毕之后,还要再单独设置指定账户、密码
我们可以从官网查找到命令,指定在安装的过程中就可以根据提示完成账户、密码的设置
安装 rabbitmq 方法二
访问:https://registry.hub.docker.com/_/rabbitmq/
找到以下文档位置
执行docker安装rabbitmq的命令,图中命令与方法一的命令整合
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
显示启动错误是因为,安装成功后会尝试启动,上面我们手动安装rabbit已经占用端口,所以需要先将rabbitmq关闭,再用docker启动
查看docker容器状态
docker ps -a
镜像当前处于创建状态下,还没有启动
根据容器ID启动rabbitmq
docker start 13493aa7dbb8
启动成功
再次访问浏览器,这次访问的是 Docker 启动的 RabbitMQ
Docker安装RabbitMQ明显比手动安装更加简洁,省去了 erlang、socat、管理界面插件等安装过程,甚至安装时就已经设置好了账户、密码,直接一步到位
注意端口的使用,一定要提前开放好
none:
management:查看自己相关节点信息
Policymaker:
Monitoring:相当于普通管理员
Administrator:超级管理员,(学习经常使用)
参考官网快速启动:https://www.rabbitmq.com/getstarted.html
构建一个maven工程,添加rabbitmq依赖
先使用原生 rabbitmq 依赖,后面再整合 Spring
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.10.0version>
dependency>
在上图的模型中,有以下概念:
生产者
// 简单模式
public class Producer {
public static void main(String[] args) {
// 1.创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("10.15.0.9");
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,发送消息和接受消息
/*
参数1:队列名称
参数2:是否持久化,非持久化消息会存盘吗?会存盘,但是会随着重启服务器而丢失
参数3:是否独占队列
参数4:是否自动删除,随着最后一个消费者消息完毕消息以后是否把队列自动删除
参数5:携带附属属性
*/
String queueName = "queue1";
channel.queueDeclare(queueName,false,false,false,null);
// 5.发送消息给队列queue,都是由channel来处理
/*
参数1:交换机
参数2:队列、路由key
参数3:消息的状态控制
参数4:消息主题
*/
//面试题:可以存在没有交换机的队列吗?不可能,虽然没有指定交换机但是一定会存在一个默认的交换机
String message = "Hello";
channel.basicPublish("", queueName, null,message.getBytes());
System.out.println("消息发送成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null && channel.isOpen()) {
try {
// 6.关闭通道,注意关闭顺序
channel.close();
// 7.关闭连接
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
启动成功后,查看管理面板,可以看到我们创建的队列,有一条消息没有被消费
随着最后一条消息被消费,非持久化的消息会被删除,持久化消息会被保存
消费者
public class Consumer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("10.15.0.9");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection("生产者");
channel = connection.createChannel();
// 确保和消费者的队列名一致才可以接收消息
String queueName = "queue1";
channel.queueDeclare(queueName,false,false,false,null);
// 接收消息,必须重写两个方法,消息的处理和异常情况的处理
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("接收消息失败");
}
});
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null && channel.isOpen()) {
try {
// 6.关闭通道,注意关闭顺序
channel.close();
// 7.关闭连接
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
启动运行,消费者成功接收消息,查看管理面板,之前存在的一条消息已经被消费
小结:
RabbitMQ的运行遵循AMQP协议
AMQP全称:Advanced Message Queuing Protocol(高级消息队列协议)。是应用层协议的一个开发标准,为面向消息的中间件设计
ACK是应答,分手动ACK、自动ACK,实际生产中一般使用手动ACK
核心概念:
注意:
正是由于交换机对消息队列有着不同的推送机制,才产生了多种模式
在交换机菜单里,我们可以看到交换机列表,其中包括了默认交换机,点击一个交换进入,
Fanout - 发布与订阅模式,是一种广播机制,它是没有路由 key的模式,即使指定了routing-key也没有意义
fanout 发布于订阅模式就好比收听广播
模拟代码的执行流程,方便我们理解程序的思路
点击Exchange菜单,创建一个 exchange,填写交换机参数,确认添加
点击Queues菜单,创建两个队列用于接收消息queue2,queue3
点击进入刚创建的队列,当前使用了默认绑定交换机,修改绑定交换机为我们创建的 fanout-exchange
查看交换机也可以看到,对应已经绑定了队列
在交换机的菜单中,选择我们床架你的交换机,发送消息,publish message,fanout模式不需要指定队列,发送即可推送到自己所绑定的全部队列中
选择一个队列点击进入,预览消息
生产者
// fanout模式
public class Producer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("10.15.0.9");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection("生产者");
channel = connection.createChannel();
// fanout 无需指定队列和路由
// 发送的消息内容
String message = "Hello fanout 代码测试";
String exchangeName = "fanout-exchange";
String routeKey = "";
String type = "fanout";
channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
System.out.println("消息发送成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null && channel.isOpen()) {
try {
// 6.关闭通道,注意关闭顺序
channel.close();
// 7.关闭连接
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
消费者
public class Consumer {
// 这里我们直接使用多线程模拟多个消费者
public static void main(String[] args) {
new Thread(runnable, "queue1").start();
new Thread(runnable, "queue2").start();
new Thread(runnable, "queue3").start();
}
private static Runnable runnable = new Runnable() {
@Override
public void run() {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("10.15.0.9");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
final String queueName = Thread.currentThread().getName();
try {
connection = connectionFactory.newConnection("生产者");
channel = connection.createChannel();
// 这里我们并没有做交换机与度列的绑定,因为在web管理端已经绑定好了
// 所以以后在实际生产中也可以使用这种图形+代码相结合的方式
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 (Exception e) {
e.printStackTrace();
} finally {
if (channel != null && channel.isOpen()) {
try {
channel.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
};
}
启动测试,先生产消息,在消费消息,由于我们在web管理端设置的fanout-exchange绑定了queue2和queue3
需要注意的是:
实际上,路由模式就是在发布与订阅模式基础上增加了一个routing-key,基于routing-key进行选择队列发送消息
创建交换机,选择direct类型
添加绑定队列,自己定义一个key
使用交换机发送消息,比如我们想指定发送消息,路由键为email的队列可以接收,也就是queue1
查看队列可以看到,拥有路由键email的queue1接收到了消息,只要队列满足路由键即可接收消息
在上面 fanout 模式的基础上,修改交换机名字,交换机类型,增加路由键的的绑定
//6.定义路由key
String routeKey = "email";
//7.指定交换机的类型
String type = "direct";
channel.basicPublish(exchangeName,routeKey, null,message.getBytes());
主题模式其实就是在fanout和direct模式的基础上进一步叠加,提供可以支持模糊匹配的路由routing-key
创建交换机,选择topic类型
添加绑定队列,定义模糊匹配的routing-key,
*
必须匹配一个级别,#
可以匹配没有也可以匹配一级或多级
发送消息,规定路由键com.course.swy
查看队列,可以看到,queue1和queue2收到了信息
定义可以模糊匹配的路由键,指定交换机类型,指定交换机名字
//6.定义路由key
String routeKey = "com.order.test.xxx";
//7.指定交换机的类型
String type = "topic";
channel.basicPublish(exchangeName,routeKey, null,message.getBytes());
```java
//5.准备交换机
String exchangeName = "direct_message_exchange";
String exchangeType = "direct";
//如果你用界面把queue和exchange的关系先绑定话,代码就不需要在编写这些声明代码可以让代码变得更简洁
//如果用代码的方式去声明,我们要学习一下
//6.声明交换机 所谓的持久化就是指,交换机会不会随着服务器重启造成丢失
channel.exchangeDeclare(exchangeName,exchangeType,true);
//7.声明队列
channel.queueDeclare("queue5",true,false,false,null);
channel.queueDeclare("queue6",true,false,false,null);
channel.queueDeclare("queue7",true,false,false,null);
//8.绑定队列和交换机的关系
channel.queueBind("queue5",exchangeName,"order");
channel.queueBind("queue6",exchangeName,"order");
channel.queueBind("queue7",exchangeName,"course");
channel.basicPublish(exchangeName,course, null,message.getBytes());
通过代码实线交换机与队列的绑定关系
如果消费者去消费一个不存在的队列将会出现异常
以 direct 模式为例
public class Producer {
public static void main(String[] args) {
// 连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.126.130");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 连接
connection = connectionFactory.newConnection("生产者");
// 信道
channel = connection.createChannel();
// 定义队列
String queueName = "queue2";
channel.queueDeclare(queueName,false,false,false,null);
// 定义交换机
String exchangeName = "direct_message_exchange";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName, exchangeType, true);//第三个参数是否持久化
// 绑定交换机和队列
channel.queueBind("queue2", exchangeName, "order");
// 绑定上面的各个参数,发送消息
String message = "Hello";
channel.basicPublish(exchangeName, queueName, null, message.getBytes());
System.out.println("消息发送成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null && channel.isOpen()) {
try {
channel.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
简单模式不需要指定交换机,直接指定队列,一个队列对应一个消费者;
工作模式不需要指定交换机,直接指定队列,一个队列对应多个消费者;
当一个队列中有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?
主要有两种模式:
不指定哪种模式的情况下,默认就是轮询模式,强调均分性;不管消费者处理速度如何,一律均分
生产者
跟简单模式一样!无需指定交换机(有默认),需要指定队列
消费者
创建两个一样的!且使用同一个队列。应答模式ack必须使用自动应答 true
生产者
与上面的生产者一样
消费者
创建两个相同的消费者,使用同一个队列,必须使用手动应答,autoAck为false,和指定指标qos
这样消费时就会切换为公平分发模式,能者多劳,处理快的消费的更多
指定qos,qos是一次从队列中取出的消息数量,不易过大,根据情况定,通常写1也够了
//简单模式
public class Consumer{
//3.接受内容
//指标定义出来
channel.basicQos(1);
channel.basicConsume("queue1",false,new DefaultConsumer(){
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(new String("收到消息是" + new String(meassage.getBody()),"UTF-8"));
//改成手动应答
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
},new CancelCallback(){
public void handle(String consumerTag) throws IOException {
System.out.println("接受失败了");
}
});
//4.关闭
channel.close();
connection.close();
}
通过设置参数的方式来匹配队列,这个参数在代码中对应了channel.basicPublish()
中的props
参数
创建交换机,指定headers类型
绑定队列,指定参数
交换机发送消息
查看队列queue1接收情况
面试技巧:
分析:
如何回答:
同步异步的问题(串行)
串行方式:
缺点:
public void makeOrder(){
//1.发送订单
//2.发送短信服务
//3.发送email服务
//4.发送app服务
}
并行方式 异步线程池
并行方式:
缺点:
public void test(){
//异步
theadpool.submit(new Callable<Object>{
//1.发送短信服务
})
//异步
theadpool.submit(new Callable<Object>{
//2.
})
//异步
theadpool.submit(new Callable<Object>{
//3.
})
//异步
theadpool.submit(new Callable<Object>{
//4.
})
}
存在问题
异步消息队列的方式
好处:
按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20QPS。比串行提高了3倍,比并行提高了两倍
好处:
按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20QPS。比串行提高了3倍,比并行提高了两倍
创建空项目,用springboot分别创建生产者和消费者的子模块module
确保rabbitmq服务已经启动,防火墙、阿里安全组已经调整完毕
生产者
核心配置:application.yml
# 服务端口
server:
port: 8080
# 不配置的话就是默认本机ip 端口5672 账户guest/guest
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 41.104.141.27
port: 5672
订单服务:OrderService.java
@Service
public class OrderService{
// 获取连接对象
@Autowired
private RabbitTemplate rabbitTemplate;
// 模拟用户下单
public void makeOrder(String userid,String productid,int num){
// 1.根据商品id查询库存是否足够
// 2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生产成功:"+orderId);
// 3.通过MQ来完成消息的分发
// 参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
String exchangeName = "fanout_order_exchange";
String routingKey = "";
rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId);
}
}
配置类:RabbitMqConfiguration
// 配置类配置具体的rabbitmq属性参数
@Configuration
public class RabbitMqConfiguration{
// 1.声明注册fanout模式的交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanout_order_exchange",true,false);
}
// 2.声明队列
@Bean
public Queue smsQueue(){
return new Queue("sms.fanout.queue",true);
}
@Bean
public Queue duanxinQueue(){
return new Queue("duanxin.fanout.queue",true);
}
@Bean
public Queue emailQueue(){
return new Queue("email.fanout.queue",true);
}
// 3.完成绑定关系
@Bean
public Binding smsBingding(){
return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
}
@Bean
public Binding duanxinBingding(){
return BindingBuilder.bind(duanxinQueue()).to(fanoutExchange());
}
@Bean
public Binding emailBingding(){
return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
}
}
消息发送测试:springboot测试类
@SpringBootTest
class RabbitmqSpringbootOrderProducerApplicationTests {
@Autowired
private OrderService orderService;
@Test
void contextLoads() {
orderService.makeOrder("1", "1", 100);
}
}
运行测试,查看管理界面,发布于订阅模式下,三个队列都能收到消息
消费者
核心配置:application.yml
# 服务端口,与生产者区分开
server:
port: 8081
# 不配置的话就是默认本机ip 端口5672 账户guest/guest
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 41.104.141.27
port: 5672
创建消费者:FanoutSmsConsumer、FanoutDuanxinConsumer、FanoutEmailConsumer
@Component
@RabbitListener(queues = {
"sms.fanout.queue"})// 对应队列名
public class FanoutSmsConsumer{
@RabbitHandler
// 该方法的参数就是接收的消息
public void reviceMessage(String message){
System.out.println("sms接收到了的订单信息是:"+message);
}
}
@Component
@RabbitListener(queues = {
"duanxin.fanout.queue"})
public class FanoutDuanxinConsumer{
@RabbitHandler
public void reviceMessage(String message){
System.out.println("duanxin接收到了的订单信息是:"+message);
}
}
@Component
@RabbitListener(queues = {
"email.fanout.queue"})
public class FanoutEmailConsumer{
@RabbitHandler
public void reviceMessage(String message){
System.out.println("email接收到了的订单信息是:"+message);
}
}
配置类:省略
启动测试:
direct模式只需要在fanout模式基础上添加一些修改
生产者
在生产模块中添加direct配置类:DirectRabbitMqConfiguration
定义三个队列,一个交换机,队列绑定交换机,队列携带路由键
// 配置类配置具体的rabbitmq属性参数
@Configuration
public class DirectRabbitMqConfiguration {
// 1.声明注册fanout模式的交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange("direct_order_exchange",true,false);
}
// 2.声明队列
@Bean
public Queue smsQueue(){
return new Queue("sms.direct.queue",true);
}
@Bean
public Queue duanxinQueue(){
return new Queue("duanxin.direct.queue",true);
}
@Bean
public Queue emailQueue(){
return new Queue("email.direct.queue",true);
}
// 3.完成绑定关系
@Bean
public Binding smsBingding(){
return BindingBuilder.bind(smsQueue()).to(directExchange()).with("sms");
}
@Bean
public Binding duanxinBingding(){
return BindingBuilder.bind(duanxinQueue()).to(directExchange()).with("duanxin");
}
@Bean
public Binding emailBingding(){
return BindingBuilder.bind(emailQueue()).to(directExchange()).with("email");
}
}
订单服务:OrderService.java
指定使用哪个路由键发送消息,哪个路由键对应的队列就会接受消息
@Service
public class OrderService {
// 获取连接对象
@Autowired
private RabbitTemplate rabbitTemplate;
// 模拟用户下单
public void makeOrder(String userid,String productid,int num) {
// 1.根据商品id查询库存是否足够
// 2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生产成功:"+orderId);
// 3.通过MQ来完成消息的分发
// 参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
String exchangeName = "direct_order_exchange";
String routingKey = "";
// 这了我们只让sms和email对应的队列接收消息
rabbitTemplate.convertAndSend(exchangeName,"email", orderId);
rabbitTemplate.convertAndSend(exchangeName,"sms", orderId);
}
}
消费者
创建消费者:DirectSmsConsumer、DirectDuanxinConsumer、DirectEmailConsumer
@Component
@RabbitListener(queues = {
"duanxin.direct.queue"})
public class DirectDuanxinConsumer {
@RabbitHandler
public void reviceMessage(String message){
System.out.println("duanxin接收到了的订单信息是:"+message);
}
}
@Component
@RabbitListener(queues = {
"email.direct.queue"})
public class DirectEmailConsumer {
@RabbitHandler
public void reviceMessage(String message){
System.out.println("email接收到了的订单信息是:"+message);
}
}
@Component
@RabbitListener(queues = {
"sms.direct.queue"})// 对应队列名
public class DirectSmsConsumer {
@RabbitHandler
// 该方法的参数就是接收的消息
public void reviceMessage(String message){
System.out.println("sms接收到了的订单信息是:"+message);
}
}
启动主启动类:只有当前所监听的队列与交换机绑定,且队列有生产者发消息时携带的路由键,才能接收消息
由于生产者的service中,只对 sms、email这两个路由键发送消息,所以email和sms分别对应这两个路由键,接收到了消息
SpringBoot除了可以使用配置类的方式定义rabbitmq相关信息,还可以使用注解的方式进行配置,这里我们用注解的方式进行配置
生产者
OrderService中添加方法,
public void makeOrderTopic(String userid,String productid,int num) {
// 1.根据商品id查询库存是否足够
// 2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生产成功:"+orderId);
// 3.通过MQ来完成消息的分发
// 参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
String exchangeName = "topic_order_exchange";
String routingKey = "com.duanxin";// 短信和sms可以收到
rabbitTemplate.convertAndSend(exchangeName,routingKey, orderId);
}
消费者(采用注解)
创建消费者:TopicSmsConsumer.java、TopicDuanxinConsumer.java、TopicEmailConsumer.java
在注解上完成队列定义、交换机绑定,队列携带的路由键
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "duanxin.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange", type = ExchangeTypes.TOPIC),
key = "#.duanxin.#"
))
public class TopicDuanxinConsumer {
@RabbitHandler
public void reviceMessage(String message){
System.out.println("duanxin接收到了的订单信息是:"+message);
}
}
@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 reviceMessage(String message){
System.out.println("email接收到了的订单信息是:"+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 = "com.#"
))
public class TopicSmsConsumer {
@RabbitHandler
// 该方法的参数就是接收的消息
public void reviceMessage(String message){
System.out.println("sms接收到了的订单信息是:"+message);
}
}
启动消费者主启动类,开始监听
运行生产者测试类 调用makeOrderTopic
方法,发送消息
根据路由键的匹配机制,duanxin和sms接收到了消息
过期时间 TTl表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置 TTL,目前有两种方法可以设置
如果上述两种方法同时使用,则消息的过期时间以两者 TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的 TTL值,就称为 dead message被投递到死信队列,消费者将无法再收到该消息
设置队列TTL
创建配置类TTLRabbitMQConfiguration.java
在消费中添加配置类,定义队列、交换机、绑定关系
@Configuration
public class TTLRabbitMQConfiguration{
// 1.声明注册direct模式的交换机
@Bean
public DirectExchange ttldirectExchange(){
return new DirectExchange("ttl_direct_exchange",true,false);}
// 2.队列的过期时间
@Bean
public Queue directttlQueue(){
// 设置过期时间
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);// 这里一定是int类型
return new Queue("ttl.direct.queue",true,false,false,args);}
@Bean
public Binding ttlBingding(){
return BindingBuilder.bind(directttlQueue()).to(ttldirectExchange()).with("ttl");
}
}
生产者添加业务方法,生产消息,在OrderService中添加
public void makeOrderTtl(String userid,String productid,int num) {
String orderId = UUID.randomUUID().toString();
System.out.println("订单生产成功:"+orderId);
String exchangeName = "ttl_order_exchange";
String routingKey = "ttl";
rabbitTemplate.convertAndSend(exchangeName,routingKey, orderId);
}
生成测试类,调用makeOrderTtl
方法,生产数据
@Test
void contextLoads2() {
orderService.makeOrderTtl("1", "1", 10);
}
查看web界面,队列多出一条消息,过了5秒,自动消失
设置消息TTL
修改 OrderService 类,
发送的消息从原来的 String 变为 MessagePostProcessor 对象
public class OrderService{
@Autowired
private RabbitTemplate rabbitTemplate;
//模拟用户下单
public void makeOrder(String userid,String productid,int num){
//1.根据商品id查询库存是否足够
//2.保存订单
String orderId = UUID.randomUUID().toString();
sout("订单生产成功:"+orderId);
//3.通过MQ来完成消息的分发
//参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
String exchangeName = "ttl_order_exchange";
String routingKey = "ttlmessage";
//给消息设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
public Message postProcessMessage(Message message){
//这里就是字符串
message.getMessageProperties().setExpiration("5000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
}
rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId,messagePostProcessor);
}
}
RabbitMqConfiguration.java
@Configuration
public class TTLRabbitMQConfiguration{
//1.声明注册direct模式的交换机
@Bean
public DirectExchange ttldirectExchange(){
return new DirectExchange("ttl_direct_exchange",true,false);}
//2.队列的过期时间
@Bean
public Queue directttlQueue(){
//设置过期时间
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl",5000);//这里一定是int类型
return new Queue("ttl.direct.queue",true,false,false,args);}
@Bean
public Queue directttlMessageQueue(){
return new Queue("ttlMessage.direct.queue",true,false,false,args);}
@Bean
public Binding ttlBingding(){
return BindingBuilder.bin(directttlMessageQueue()).to(ttldirectExchange()).with("ttlmessage");
}
}
DLX,全称 Dead-Letter-Exchange
,可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX的队列就称之为死信队列。消息变成死信,可能是由于以下原因:
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性,当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的 DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange
指定交换机即可
定义死信交换机:DeadRabbitMqConfiguration.java
@Configuration
public class DeadRabbitMqConfiguration{
// 1.声明注册direct模式的交换机
@Bean
public DirectExchange deadDirect(){
return new DirectExchange("dead_direct_exchange",true,false);}
// 2.队列的过期时间
@Bean
public Queue deadQueue(){
return new Queue("dead.direct.queue",true);}
@Bean
public Binding deadbinds(){
return BindingBuilder.bind(deadDirect()).to(deadQueue()).with("dead");
}
}
修改正常的交换机配置:TTLRabbitMQConfiguration
正常的ttl交换处理消息,如果发生超时,则交给死信交换机,进入死信队列,进而下一步操作
如果是direct模式,那么死信队列也要有key,如果是fanout模式,则死信队列不需要key
@Configuration
public class TTLRabbitMQConfiguration{
// 1.声明注册direct模式的交换机
@Bean
public DirectExchange ttldirectExchange(){
return new DirectExchange("ttl_direct_exchange",true,false);}
// 2.队列的过期时间
@Bean
public Queue directttlQueue(){
//设置过期时间
Map<String,Object> args = new HashMap<>();
//args.put("x-max-length",5);
args.put("x-message-ttl",5000);// 超时设置,这里一定是int类型
args.put("x-dead-letter-exchange","dead_direct_exchange");// 绑定死信交换机
args.put("x-dead-letter-routing-key","dead");// 路由key,fanout不需要配置
return new Queue("ttl.direct.queue",true,false,false,args);}
@Bean
public Queue directttlMessageQueue(){
return new Queue("ttlMessage.direct.queue",true,false,false,args);}
@Bean
public Binding ttlBingding(){
return BindingBuilder.bin(directttlMessageQueue()).to(ttldirectExchange()).with("ttlmessage");
}
}
配置了死信队列,消息超时之后自动进入死信队列,这是消息的一种可靠机制,直接移除是很危险的,
当内存使用超过配置的阈值或者磁盘空间剩余空间大于剩余的阈值时,RabbitMQ会暂时阻塞客户端连接,并且停止接收从客户端发来的消息,一次避免服务器的崩溃,客户端与服务端的心跳检测机制也会失效
memory 为105MB,表示当前rabbitmq服务使用内存为105mb,下面389MB high waterm 表示如果rabbitmq使用用内存达到这个阈值,就会触发警告,随后 connections 菜单中的所有的链接就会变成阻塞,即 block,生产者无法再将消息存储到消息队列中;
接下来必须尽快将内存上线调高,或者增加内存,也可能是程序中出现了死循环,
消息从内存转移到磁盘的过程,叫做消息的持久化,涉及到持久队列,
rabbitmq的内存上线阈值默认是物理内存的0.4倍,本人的虚拟机内存为1GB,所以这里的阈值389MB就是这么来的
出现内存或磁盘不足的时候应该尽快调整,避免影响生产环境的使用
当出现警告的时候,可以通过配置去修改和调整
方式一:通过命令
rabbitmqctl set_vm_memory_high_watermark <fraction> #表示相对值
rabbitmqctl set_vm_memory_high_watermark absolute 50MB #对应绝对值
fraction/value 为内存阈值,fraction为相对值,absolute后面写绝对值。默认情况是:0.4或2GB,代表的含义是:当 RabbitMQ使用的内存超过40%或value时,就会产生警告并且会阻塞所有生产者的连接。两种命令只需执行一个即可。
比如,这里将阈值设置为50MB,当前使用了104MB已经超过阈值了,爆红
方式二:修改配置文件 rabbitmq.conf
修改配置文件:/etc/rabbitmq/rabbitmq.conf
#默认
#vm_memory_high_watermark.relative = 0.4
#使用ralative相对值进行设置fraction,建议取值在0.4-0.7之间,不建议超过0.7
vm_memory_high_watermark.relative = 0.6
#使用absolute的绝对值方式,单位KB,MB,GB
vm_memory_high_watermark.absolute = 2GB
注意:
在某个Broker节点及内存阻塞生产者之前,他会尝试将队列中的消息换页到磁盘中以释放内存空间,持久化和非持久化的消息都会写入磁盘中,其中持久化的消息本身就在磁盘中的一个副本,所以在转移的过程中持久化的消息会先从内存中清除掉。
命令修改:
vm_memory_high_watermark_paging_ratio = 0.7 # 阈值的0.7时换页
这个值要小于1,如果设置为大于等于1,那么内存使用都已经积攒到阈值了,就已经阻塞了,再换页就没有意义了,通常为0.7
当磁盘的剩余空间低于确定的阈值时,rabbitmq同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务器崩溃。
比如上图disk space表示,当前的磁盘阈值是93GB,而磁盘剩余空间为35GB,35<93所以就会爆红
命令修改方式:
rabbitmqctl set_disk_free_limit <disk_limit># 绝对值
rabbitmqctl set_disk_free_limit memory_limit <fraction># 相对值
RabbitMQ这款消息队列中间件本身基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering,这使得RabbitMQ本身不需要向ActiveMQ、Kafka那样通过Zookeeper分别实现高可用方案和保存集群的元数据,集群是保证可靠的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的
在实际使用中采取多机多实例部署方式,为了便于我们学习搭建,这里我们主要针对单机多实例来演示
配置集群的前提是 rabbitmq 可以运行起来,
使用命令ps aix|grep rebbitmq
查看相关进程,命令rabbitmqct status
你可以查看rabbitmq状态信息而不报错:
通常集群的搭建至少需要三个节点,这里我们搭建两个,rabbit-1和rabbit-2,1作为主节点maste,演示够用了
关闭原有的rabbitmq
先将本机上的单机rabbitmq关闭,
systemctl stop rabbitmq-server
如果是docker启动的话,则通过docker的方式来关闭
docker stop 容器号
systemctl stop docker
这里我们使用普通安装的rabbitmq演示,没有使用docker启动
创建新的节点
创建并且启动rabbit-1
sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start &
创建并启动rabbit-2
因为部署在一台机器上,所以端口号要分开,web端15673也是为了区分开
sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start &
ps aux|grep rabbitmq
绑定节点关系
重新启动,绑定主从关系,1为主节点,2为从节点
#停止应用
sudo rabbitmqctl -n rabbit-1 stop_app
#清除节点上的历史数据,如果不清除,无法将节点加入到集群,因为集群的数据是共享的
sudo rabbitmqctl -n rabbit-1 reset
#启动应用
sudo rabbitmqctl -n rabbit-1 start_app
#停止应用
sudo rabbitmqctl -n rabbit-2 stop_app
#清除节点上的历史数据,如果不清除,无法将节点加入到集群,因为集群的数据是共享的
sudo rabbitmqctl -n rabbit-2 reset
#将rabbit-2节点加入到rabbit-1主节点集群中,Server-node是服务器的主机名,就是左侧root@后面那个,比如我的就是localhost,
sudo rabbitmqctl -n rabbit-2 join_cluster rabbit-1@Server-node
#启动应用
sudo rabbitmqctl -n rabbit-2 start_app
整个集群构建完毕,这种集群就是第一章提到的 集群模式1 - Master-slave主从共享数据的部署方式
验证集群状态
sudo rabbitmqctl cluster_status -n rabbit-1
Web端监控
默认web端监控是关闭的,我们将它打开
rabbitmq-plugins enable rabbitmq_management
提前设置防火墙和阿里安全组开放相关端口15672/15673/5672/5673。。。
设置并授权用户
比单机启动多了-n
rabbitmqctl -n rabbit-1 add_user admin admin
rabbitmqctl -n rabbit-1 set_user_tags admin administrator
rabbitmqctl -n rabbit-1 set_permissions -p / admin ".*" ".*" ".*"
rabbitmqctl -n rabbit-2 add_user admin admin
rabbitmqctl -n rabbit-2 set_user_tags admin administrator
rabbitmqctl -n rabbit-2 set_permissions -p / admin ".*" ".*" ".*"
访问web端,15672和15673两个端口,
此时,我们在web端主节点和从节点的操作都会共享同步,包括创建队列、交换机、绑定、发消息等等
如果关闭从节点rabbit-2,从节点web端则无法访问,主节点有提示
关闭主节点也是如此,关闭节点,已经创建的队列、交换机也依然存在
一主多从,分别在不同的机器上时
概述:
两个独立的服务现在要组成一个整体,需要解决数据的一致性问题,比如下单后,库存异常导致下单失败,那么下单的数据也有好回滚
这是跨JVM的事务
Spring提供的事务支持智能控制当前JVM内级别的控制,无法控制其他JVM的事务
一、两阶段提交(2PC)需要数据库生产商的支持,Java组件有atomikos支持等
两阶段提交,通过引入协调者Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行,
这是早期出现的解决方案
1.准备阶段
协调者询问参与者事务是否执行成功,参与者发挥事务指定的结果
如果事务在每个参与者上执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交,只有在提交阶段接收到协调者发来的通知后,才进行事务的提交或回滚
二、补偿事务(TCC)严选、阿里、蚂蚁金服
TCC其实是采用补偿机制,核心思想为,针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作,分三个阶段
举个例子,Bob向Smith转账
优点:
缺点:
三、本地消息表(异步确保),支付宝、微信支付主动查询支付状态,对账的形式
本地消息表与业务数据表处于同一个数据库中,这样能利用本地事务来保证在这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
一方完成操作都要通过消息队列和其他方对账,出现问题就广播通知所有方
优点:
缺点:
四、MQ事务消息,异步场景,通用较强,拓展性较高(本课程介绍的方法)
有些第三方MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式类似于采用第二阶段的提交,但是市面上的一些主流MQ都不支持事务消息,比如Kafka
优点:
缺点:
小结:
整体架构
项目准备,订单业务和派单业务分别准备数据库和实体,并且程序可以启动成功
创建订单,本地数据库保存提交信息,
本地提交后,还要调用运单系统,将订单号传过去
如果,调用运单失败,createOrder就会抛出异常,触发注解事务,@Transactional,回滚事务
现在的这个事务是订单系统的事务,如果调用运单出现问题,订单可以回滚;
但是如果运单系统内部的运行就不受订单的事务控制了,这样就可能导致数据不一致;
如何解决上述问题,需要分布式事务来处理
增加消息冗余,
注意:将原来的@Transtraction去掉,因为现在我们开始用分布式事务
生产者接收RabbitMQ的回执,确定是否执行成功
@PostConstruct修饰的regCallback方法会在 RabbitTemplate构造函数之后执行,当然也是发生在sendMessage调用队列之前完成的
这样调用队列的时候,成功还是失败都会通过这个回执可以接收到
还需要配置yml,开启确认机制
publisher-confirm-type: correlated
当消费者消费消息出现异常时,消息队列会不停的重发消息,触发死循环,冲垮服务器,消耗完内存,导致宕机
几种解决方案:
第一种方式:控制重试次数,
配置重试,修改yml
第三种方式,try catch+手动ack+死信队列+人工干预
配置yml,开启手动ack(默认none是自动ack)
这种方式需要关闭失败后的重发(requeue设置为false),然后出现问题的消息就会丢进死信队列,
如果开启重发,一旦消费出现异常还是会死循环;我们已经设置了重试次数,为什么还会死循环?因为try catch会将重试次数的机制给屏蔽掉;
所以添加了try catch以后,就不要开启重试,正常解决异常即可
抓住异常之后如何解决?用死信队列(DLX)来解创建一个新的消费者监听处置死信队列,处置以后也要及时移除,避免死信队列的堆积
当然,因为数据有重发机制,保存过程中还需要考虑幂等性问题,避免数据的重复,可以在数据库设置主键的方式解决,或者分布式锁解决
参考资料:
RabbitMQ中文文档:http://rabbitmq.mr-ping.com/
教学视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=1