修改时间:2020年3月10日
作者:pp_x
邮箱:pp_x12138@163.com
什么是RabbitMQ
MQ(MessageQueue)消息队列
- 消息队列中间件,是分布式系统中的重要组件
- 主要解决,异步处理,应用解耦,流量削峰等问题
- 从而实现高性能,高可用,可伸缩和最终一致性的架构
- 使用较多的消息队列产品:RabbitMQ,RocketMQ,ActiveMQ,ZeroMQ,Kafka等
异步处理
- 用户注册后,需要发送验证邮箱和手机验证码;
- 将注册信息写入数据库,发送验证邮件,发送手机,三个步骤全部完成后,返回给客户端
应用解耦
- 场景:订单系统需要通知库存系统
- 如果库存系统异常,则订单调用库存失败,导致下单失败
- 原因:订单系统和库存系统耦合性太大
- 原因:订单系统和库存系统耦合性太大
- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户,下单成功
- 库存系统:订阅下单的消息,获取下单信息,库存系统根据下单信息,再进行库存操作;
- 假如:下单的时候,库存系统不能正常运行,也不会影响下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现了订单系统和库存系统的应用解耦;
- 所以说,消息队列是典型的:生产者消费者模型
- 因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的入侵,这样就实现了生产者和消费者的解耦
流量削峰
- 抢购,秒杀等业务,针对高并发的场景
- 因为流量过大,暴增会导致应用挂掉,为解决这个问题,在前端加入消息队列
- 用户的请求首先写入消息队列,当队列达到规定长度时,将不在接收消息的写入,即秒杀成功的就是进入消息队列的
背景知识
AMQP高级消息队列协议
- 即
Advanced Message Queuing Protocol
,一个提供统一消息服务的应用层标准高级消息队列协议 - 协议:数据在传输的过程中必须要遵守的规则
- 基于此协议的客户端可以与消息中间件传递消息
- 并不受产品、开发语言等条件的限制
JMS
Java Message Server
,Java消息服务应用程序接口, 一种规范,和JDBC担任的角色类似- 是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信
二者的联系
- JMS是定义了统一接口,统一消息操作;AMQP通过协议统一数据交互格式
- JMS必须是java语言;AMQP只是协议,与语言无关
Erlang语言
- Erlang(['ə:læŋ])是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信所辖CS-Lab开发,目的是创造一种可以应对大规模并发活动的编程语言和运行环境
- 最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合构建分布式,实时软并行计算系统
- Erlang运行时环境是一个虚拟机,有点像Java的虚拟机,这样代码一经编译,同样可以随处运行
Rabbit的优势
- Erlang开发,AMQP的最佳搭档,安装部署简单,上手门槛低
- 企业级消息队列,经过大量实践考验的高可靠,大量成功的应用案例,例如阿里、网易等一线大厂都有使用
- 有强大的WEB管理页面
- 强大的社区支持,为技术进步提供动力
- 支持消息持久化、支持消息确认机制、灵活的任务分发机制等,支持功能非常丰富
- 集群扩展很容易,并且可以通过增加节点实现成倍的性能提升
Rabbit组件功能
- Broker:消息队列服务器实体
- Virtual Host:虚拟主机
- 标识一批交换机、消息队列和相关对象,形成的整体
- 虚拟主机是共享相同的身份认证和加密环境的独立服务器域
- 每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权
限机制 - vhost是AMQP概念的基础,RabbitMQ默认的vhost是 /,必须在链接时指定
- Exchange:交换器(路由)
- 用来接收生产者发送的消息并将这些消息路由给服务器中的队列
- Queue:消息队列
- 用来保存消息直到发送给消费者。
- 它是消息的容器,也是消息的终点
- 一个消息可投入一个或多个队列
- 消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Banding:绑定,用于消息队列和交换机之间的关联。
- Channel:通道(信道)
- 多路复用连接中的一条独立的双向数据流通道
- 信道是建立在真实的TCP连接内的虚拟链接
- AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,都是通过信道完成的
- 因为对于操作系统来说,建立和销毁TCP连接都是非常昂贵的开销,所以引入了信道的概念,用来复用TCP连接。
- Connection:网络连接,比如一个TCP连接。
- Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
- Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
- Message:消息
- 消息是不具名的,它是由消息头和消息体组成
- 消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(优先级)、delivery-mode(消息可能需要持久性存储[消息的路由模式])等。
RabbitMQ的使用
- 想要安装RabbitMQ,必须先安装erlang语言环境,类似安装tomcat,必须先安装J
- 查看匹配的版本:https://www.rabbitmq.com/which-erlang.html
RabbitMQ的安装和启动
下载地址
- erlang下载:https://dl.bintray.com/rabbitmq-erlang/rpm/erlang
- socat下载:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
- RabbitMQ下载:https://www.rabbitmq.com/install-rpm.html#downloads
安装
[root@localhost opt]# rpm -ivh erlang-21.3.8.16-1.el7.x86_64.rpm
[root@localhost opt]# rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
[root@localhost opt]# rpm -ivh rabbitmq-server-3.8.6-1.el7.noarch.rpm
启动后台管理插件
[root@localhost opt]# rabbitmq-plugins enable rabbitmq_management
启动RabbitMQ
[root@localhost opt]# systemctl start rabbitmq-server.service
[root@localhost opt]# systemctl status rabbitmq-server.service
[root@localhost opt]# systemctl restart rabbitmq-server.service
[root@localhost opt]# systemctl stop rabbitmq-server.service
查看进程
[root@localhost opt]# ps -ef | grep rabbitmq
测试
- 关闭防火墙:
systemctl stop firewalld
- 浏览器输入:http://ip:15672
- 默认帐号密码:
guest
,guest用户默认不允许远程连接- 创建账号\设置用户角色\设置用户权限\
[root@localhost opt]# rabbitmqctl add_user ppx 123456 [root@localhost opt]# rabbitmqctl set_user_tags ppx administrator [root@localhost opt]# rabbitmqctl set_permissions -p "/" ppx".*" ".*" ".*"
- 查看当前用户和角色\修改密码
[root@localhost opt]# rabbitmqctl list_users [root@localhost opt]# rabbitmqctl change_password ppx 123123
- 端口:
- 5672:RabbitMQ提供给编程语言客户端链接的端口
- 15672:RabbitMQ管理界面的端口
- 25672:RabbitMQ集群的端口
快速入门
pom依赖
RabbitMQ模式
- RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此我们只学习前5种
- 在线手册:https://www.rabbitmq.com/getstarted.html
- 5种消息模型,大体分为两类
- p2p(点对点) :
- 发布订阅模式
- 点对点模式:
P2P(point to point)
模式包含三个角色- 消息队列(queue),发送者(sender),接收者(receiver)
- 每个消息发送到一个特定的队列中,接收者从中获得消息
- 队列中保留这些消息,直到他们被消费或超时
- 特点:
- 每个消息只有一个消费者,一旦消费,消息就不在队列中了
- 发送者和接收者之间没有依赖性,发送者发送完成,不管接收者是否运行,都不会影响消息发送到队列中
- 接收者成功接收消息之后需向对象应答成功(确认)
- 如果希望发送的每个消息都会被成功处理,那需要P2P
- 发布订阅模式 :
publish(Pub)/subscribe(Sub)
- pub/sub模式包含三个角色:交换机(exchange),发布者(publisher),订阅者(subcriber)
- 多个发布者将消息发送交换机,系统将这些消息传递给多个订阅者
- 特点:
- 每个消息可以有多个订阅者
- 发布者和订阅者之间在时间上有依赖,对于某个交换机的订阅者,必须创建一个订阅后,才能消费发布者的消息
- 为了消费消息,订阅者必须保持运行状态;类似于,看电视直播
- 如果希望发送的消息被多个消费者处理,可采用发布订阅模式
简单模式
RabbitMQ是一个消息代理:它接收和转发消息。你可以把它想象成一个邮局:当你把你想要寄的邮件放到一个邮箱里,你可以确定邮递员先生或女士最终会把邮件送到你的收件人那里。在这个类比中,RabbitMQ是一个邮箱、一个邮局和一个邮递员
- RabbitMQ本身只是接收,存储和转发消息,并不会对信息进行处理!
生产者
public class Sender {
public static void main(String[] args) throws Exception {
String msg = "ppx,你好";
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、创建信道
Channel channel = connection.createChannel();
//3、创建消息队列
/**
* 参数一:队列的名称
* 参数二:队列中的数据是否持久化
* 参数三:是否排外(是否支持扩展, 是否当前队列只能自己用)
* 参数四:是否自动删除(当队列的连接数为0时,队列会销毁,不管队列中是否保存数据)
* 参数五:队列参数(没有参数即为null)
*/
channel.queueDeclare("queue1",false,false,false,null);
//4、向指定的队列发送消息
/**
* 参数一:交换机名称,由于当前是简单模式也就是p2p模式 无交换机
* 参数二:目标队列的名称
* 参数三:设置消息的属性(无消息,没有属性则为空)
* 参数四:消息的内容
*/
channel.basicPublish("","queue1",null,msg.getBytes());
System.out.println("发送了"+msg);
//5、释放资源
channel.close();
connection.close();
}
}
消费者
public class Recer {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override//交付处理(收件人信息,请求头(包裹上的快递标签),协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是队列中获取的字节数组(消息)
String str = new String(body);
System.out.println("接收到:"+str);
}
};
//4、监听队列 (true 自动消息确认 ) //一直在监听
channel.basicConsume("queue1",true,consumer);
}
}
消息确认机制ACK
- 通过刚才的案例可以看出,消息一旦被消费,消息就会立刻从队列中移除
- RabbitMQ如何得知消息被消费者接收?
- 如果消费者接收消息后,还没执行操作就抛异常宕机导致消费失败,但是RabbitMQ无从得知,这样消息就丢失了
- 因此,RabbitMQ有一个ACK机制,当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收
- ACK:(Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误我们在使用http请求时,http的状态码200就是告诉我们服务器执行成功
- 这种回执ACK分为两种情况
- 自动ACK:消息接收后,消费者立刻自动发送ACK(快递放在快递柜)
- 手动ACK:消息接收后,不会发送ACK,需要手动调用(快递必须本人签收)
- 两种情况如何选择,需要看消息的重要性:
- 如果消息不太重要,丢失也没有影响,自动ACK会比较方便
- 如果消息非常重要,最好消费完成手动ACK,如果自动ACK消费后,RabbitMQ就会把消息从队列中删除,如果此时消费者抛异常宕机,那么消息就永久丢失了
- 修改手动消息确认
// false:手动消息确认
channel.basicConsume("queue1", false, consumer);
public class RecerAck {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override//交付处理(收件人信息,请求头(包裹上的快递标签),协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是队列中获取的字节数组(消息)
String str = new String(body);
System.out.println("接收到:"+str);
//手动确认(收件人信息 是否同时确认多个消息 )
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//4、监听队列 (true 自动消息确认 ) //一直在监听
channel.basicConsume("queue1",false,consumer);
}
}
工作队列模式
- 按简单模式来说,一个消费者来处理消息,如果生产者生产消息过快过多,而消费者的能
力有限,就会产生消息在队列中堆积(生活中的滞销)
生产者
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queue2",false,false,false,null);
for (int i = 0;i<=100;i++){
String msg = "ppx,你好"+i;
channel.basicPublish("","queue2",null,msg.getBytes());
System.out.println("发送了"+msg);
}
channel.close();
connection.close();
}
}
消费者1
ublic class Recer1 {
static int i = 1;
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//此方法有双重作用,如果队列不存在就创建一个队列,如果存在就获取队列
channel.queueDeclare("queue2",false,false,false,null);
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override//交付处理(收件人信息,请求头(包裹上的快递标签),协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是队列中获取的字节数组(消息)
String str = new String(body);
System.out.println("【一号】接收到:"+str+"总共接收了["+i+++"]个");
//模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动确认(收件人信息 是否同时确认多个消息 )
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//4、监听队列 (true 自动消息确认 ) //一直在监听
channel.basicConsume("queue2",false,consumer);
}
}
消费者2
public class Recer2 {
static int i = 1;
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//此方法有双重作用,如果队列不存在就创建一个队列,如果存在就获取队列
channel.queueDeclare("queue2",false,false,false,null);
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override//交付处理(收件人信息,请求头(包裹上的快递标签),协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是队列中获取的字节数组(消息)
String str = new String(body);
System.out.println("【二号】接收到:"+str+"总共接收了["+i+++"]个");
//模拟网络延迟
try {
Thread.sleep(900);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动确认(收件人信息 是否同时确认多个消息 )
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//4、监听队列 (true 自动消息确认 ) //一直在监听
channel.basicConsume("queue2",false,consumer);
}
}
测试
- 先运行两个消费者,再运行生产者,消费者需要能开启队列,如果有队列则获取,如果没有则创建,如果不声明队列会报异常
- 会发现虽然两个消费者消费速度不一致,但是消费数量一致
能者多劳原则
引用自官网
您可能已经注意到分派仍然不能完全按照我们的要求工作。例如,如果有两个员工,当所有奇怪的消息都很重,甚至消息都很轻时,一个员工会一直很忙,而另一个人几乎什么工作都不做。好吧,RabbitMQ对此一无所知,它仍然会均匀地分派消息。 这是因为RabbitMQ只在消息进入队列时发送消息。它不查看用户未确认消息的数量。它只是盲目地将每条第n个消息分派给第n个消费者。 为了克服这个问题,我们可以使用设置为prefetchCount = 1的basicQos方法。这告诉RabbitMQ一次不要给一个worker发送一条以上的消息。或者,换句话说,在worker处理并确认前一个消息之前,不要向它发送新消息。相反,它将把它分派到下一个不繁忙的worker。
- 能者多劳必须要配合手动的ACK机制才生效
// 可以理解为:快递一个一个送,送完一个再送下一个,速度快的送件就多
channel.basicQos(1);
面试题
- 如何避免消息堆积
- 采用workqueue模式,多个消费者监听同一个队列
- 接到消息后,通过线程池,异步消费
发布订阅模式
在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都被准确地交付给一个工作者。在这一部分中,我们将做一些完全不同的事情——将消息传递给多个消费者。此模式称为“发布/订阅”。
为了演示这个模式,我们将构建一个简单的日志记录系统。它将由两个程序组成——第一个将发送日志消息,第二个将接收和打印它们。在我们的日志系统中,接收程序的每一个正在运行的副本都将获得消息。这样我们就可以运行一个接收器并将日志指向磁盘;与此同时,我们可以运行另一个接收器并在屏幕上看到日志。基本上,发布的日志消息将广播到所有接收方。
- 如同生活中的抖音快手订阅号等
- x可以代表视频up主,红色可以视为粉丝队列,binding视为关注
- p生产者发送消息给x路由,x将信息转发给绑定的x队列
- X队列将信息通过信道发送给消费者,从而进行消费
- 整个过程,必须先创建路由
- 路由在生产者程序中创建
- 因为路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没有队列,路由并不知道将信息发送给谁
- 运行程序的顺序
- MessageSender(开启路由)
- MessageReceiver1和MessageReceiver2(待接收消息)
- MessageSender (发送消息)
生产者
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明路由(路由名 路由类型)
//fanout:不处理路由键 只需要将队列绑定到路由上 发送到路由的消息都会被转发到与该路由绑定到所有的队列上
channel.exchangeDeclare("test_exchange_fanout","fanout");
//队列由消费者声明,因为不确定有几个队列
//channel.queueDeclare("ps_queue",false,false,false,null);
String msg = "ppx,你好";
//此模式路由键键名为空
channel.basicPublish("test_exchange_fanout","",null,msg.getBytes());
System.out.println("生产者:"+msg);
channel.close();
connection.close();
}
}
消费者1
public class Recer1 {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("test_exchange_fanout_queue1",false,false,false,null);
//绑定路由 关注博主 (队列名、路由名、路由键)
channel.queueBind("test_exchange_fanout_queue1","test_exchange_fanout","");
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body);
System.out.println("消费者1:"+str);
}
};
channel.basicConsume("test_exchange_fanout_queue1",true,consumer);
}
}
消费者2
public class Recer2 {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("test_exchange_fanout_queue2",false,false,false,null);
//绑定路由 关注博主
channel.queueBind("test_exchange_fanout_queue2","test_exchange_fanout","");
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body);
System.out.println("消费者2:"+str);
}
};
channel.basicConsume("test_exchange_fanout_queue2",true,consumer);
}
}
路由模式
- 路由模式和发布订阅模式类似,区别是会定向发送
- 可以理解为是快递公司的分拣中心,整个小区,东面的楼小张送货,西面的楼小王送货
生产者
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明路由(路由名 路由类型)
//direct:根据路由键进行定向分发消息
channel.exchangeDeclare("test_exchange_direct","direct");
// channel.queueDeclare("ps_queue",false,false,false,null);
String msg = "[用户注册],[userId = 1001]";
//insert 路由键键名
channel.basicPublish("test_exchange_direct","select",null,msg.getBytes());
System.out.println("【用户系统】:"+msg);
channel.close();
connection.close();
}
}
消费者1
public class Recer1 {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("test_exchange_direct_queue1",false,false,false,null);
//绑定路由 关注博主 如果路由键时添加删除修改的话 绑定到此队列上 给定对应的路由键,只有路由键和发送者发送消息一致才会接收到消息
channel.queueBind("test_exchange_direct_queue1","test_exchange_direct","insert");
channel.queueBind("test_exchange_direct_queue1","test_exchange_direct","update");
channel.queueBind("test_exchange_direct_queue1","test_exchange_direct","delete");
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body);
System.out.println("消费者1:"+str);
}
};
channel.basicConsume("test_exchange_direct_queue1",true,consumer);
}
}
消费者2
public class Recer2 {
public static void main(String[] args) throws Exception {
//1、获得链接
Connection connection = ConnectionUtil.getConnection();
//2、获得信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("test_exchange_direct_queue2",false,false,false,null);
//绑定路由 关注博主
channel.queueBind("test_exchange_direct_queue2","test_exchange_direct","select");
//3、从信道中获得消息数据
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body);
System.out.println("消费者1:"+str);
}
};
channel.basicConsume("test_exchange_direct_queue2",true,consumer);
}
}
运行程序的顺序
- 先运行一次sender(创建路由器),
- 有了路由器之后,在创建两个Recer1和Recer2,进行队列绑定
- 再次运行sender,发出消息
通配符模式
- 和路由模式几乎一样,路由模式定向匹配,此模式模糊匹配
- 匹配符号
*
:只能匹配一个词(正好一个词,多一个不行,少一个也不行)#
:匹配0个或更多个词
- 官网案例
- Q1绑定了路由键 .orange. Q2绑定了路由键 ..rabbit 和 lazy.#
- 下面生产者的消息会被发送给哪个队列?
quick.orange.rabbit # Q1 Q2 lazy.orange.elephant # Q1 Q2 quick.orange.fox # Q1 lazy.brown.fox # Q2 lazy.pink.rabbit # Q2 quick.brown.fox # 无 orange # 无 quick.orange.male.rabbit # 无
生产者
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明路由(路由名,路由类型,持久化)
// topic:模糊匹配的定向分发
channel.exchangeDeclare("test_exchange_topic", "topic", true);
String msg = "商品降价";
channel.basicPublish("test_exchange_topic", "product.price", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
System.out.println("[用户系统]:" + msg);
channel.close();
connection.close();
}
}
消费者1
public class Recer1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列( 第二个参数为true:支持持久化)
channel.queueDeclare("test_exchange_topic_queue_1",true,false,false,null);
// 绑定路由(绑定 用户相关 的消息)
channel.queueBind("test_exchange_topic_queue_1", "test_exchange_topic", "user.#");
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("【消费者1】 = " + s);
}
};
// 4.监听队列 true:自动消息确认
channel.basicConsume("test_exchange_topic_queue_1", true,consumer);
}
}
消费者2
public class Recer2 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare("test_transaction_queue",false,false,false,null);
// 绑定路由(绑定 商品和订单相关 的消息)
channel.queueBind("test_transaction_queue", "test_transaction", "product.#");
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("【消费者】 = " + s);
}
};
channel.basicConsume("test_transaction_queue", true,consumer);
}
}
持久化
- 消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何避免消息丢失?
- 消费者的ACK确认机制,可以防止消费者丢失消息
- 万一在消费者消费之前,RabbitMQ服务器宕机了,那消息也会丢失
- 想要将消息持久化,那么 路由和队列都要持久化 才可以
生产者
// 声明路由(路由名,路由类型,持久化)
channel.exchangeDeclare("test_exchange_topic", "topic",true);
// 发送消息(第三个参数作用是让消息持久化)
channel.basicPublish("test_exchange_topic", "product.price", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
消费者
// 声明队列( 第二个参数为true:支持持久化)
channel.queueDeclare("test_exchange_topic_queue_1",true,false,false,null)
Spring整合RabbitMQ
- 五种消息模型,在企业中应用最广泛的就是最后一种:定向匹配topic
Spring AMQP
是基于 Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO的消息监听等,简化了我们对于RabbitMQ相关程序的开发。
生产者工程
- 依赖
<dependencies>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
spring-rabbitmq-producer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--配置连接-->
<rabbit:connection-factory id="connectionFactory"
host="192.168.227.128"
port="5672"
username="ppx"
password="123456"
virtual-host="/lagou"/>
<!--配置队列-->
<rabbit:queue name="test_spring_queue_1"></rabbit:queue>
<!--配置rabbitAdmin:主要用于在Java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--配置交换机 topic-->
<rabbit:topic-exchange name="spring_topic_exchange">
<rabbit:bindings>
<!--绑定队列-->
<rabbit:binding pattern="msg.#" queue="test_spring_queue_1"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--配置json转换工具-->
<bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>
<!--配置rabbit模板接收发送消息-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"/>
</beans>
- 发消息
public class Sender {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-produce.xml");
//从容器中获取rabbit模板对象
RabbitTemplate template = context.getBean(RabbitTemplate.class);
//3、发消息
HashMap<String, String> map = new HashMap<String, String>();
map.put("name","皮皮潇123");
map.put("phone","123465");
// template.convertAndSend("msg.user",map);
template.convertAndSend("msg.user",map);
System.out.println("消息已发出");
context.close();
}
}
消费端工程
- 依赖
与生产端一致
spring-rabbitmq-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置连接-->
<rabbit:connection-factory id="connectionFactory" host="192.168.227.128" port="5672" username="ppx" password="123456" virtual-host="/lagou"/>
<!--配置队列-->
<rabbit:queue name="test_spring_queue_1"></rabbit:queue>
<!--配置rabbitAdmin:主要用于在Java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--注解扫描-->
<context:component-scan base-package="listener"/>
<!--配置监听-->
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener ref="conListener" queue-names="test_spring_queue_1"/>
</rabbit:listener-container>
</beans>
MessageListener
接口用于spring容器接收到消息后处理消息- 如果需要使用自己定义的类型 来实现 处理消息时,必须实现该接口,并重写
onMessage()
方法 - 当spring容器接收消息后,会自动交由onMessage进行处理
- 处理消息类
/**
* 监听消费者
*/
@Component
public class ConsumerListener implements MessageListener {
//jackson提供序列化和反序列化中使用最多的类 ,用来转换json
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override//message是生产者发送过来的msg
public void onMessage(Message message) {
//将message对象转换成json
try {
JsonNode jsonNode = MAPPER.readTree(message.getBody());
String name = jsonNode.get("name").asText();
String phone = jsonNode.get("phone").asText();
System.out.println("从队列中获取的name:"+name+" phone:"+phone);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 启动项目
public class TestRunner {
public static void main(String[] args) throws IOException {
//获得容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-produce.xml");
//让程序一直运行
System.in.read();
}
}
消息成功确认机制
- 在实际场景下,有的生产者发送的消息是必须保证成功发送到消息队列中,那么如何保证成功投递呢
- 事务机制
- 发布确认机制
事务机制
- AMQP协议提供的一种保证消息成功投递的方式,通过信道开启
transactional
模式 - 并利用信道 的三个方法来实现以事务方式 发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递
channel.txSelect()
: 开启事务channel.txCommit()
:提交事务channel.txRollback()
:回滚事务
生产者
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("test_transaction", "topic");
channel.txSelect();//开启事务
try {
channel.basicPublish("test_transaction", "product.price", null, "商品1降价".getBytes());
int i = 1/0;
channel.basicPublish("test_transaction", "product.price", null, "商品2降价".getBytes());
System.out.println("生产者发送了");
channel.txCommit();//事务提交
} catch (IOException e) {
System.out.println("数据全部撤销");
channel.txRollback();
e.printStackTrace();
} finally {
channel.close();
connection.close();
}
}
}
Confirm发布确认机制
- RabbitMQ为了保证消息的成功投递,采用通过AMQP协议层面为我们提供事务机制的方案,但是采用事务会大大降低消息的吞吐量
- 开启事务性能最大损失超过250倍
- 事务效率为什么会这么低呢?试想一下:10条消息,前9条成功,如果第10条失败,那么9条消息要全部撤销回滚。太太太浪费
- 而confirm模式则采用补发第10条的措施来完成10条消息的送达
spring中使用confirm
spring-rabbitmq-producer.xml
<!--1.配置连接,启动生产者确认机制: publisher-confirms="true"-->
<rabbit:connection-factory id="connectionFactory"
host="192.168.227.128"
port="5672"
username="ppx"
password="123456"
virtual-host="/lagou"
publisher-confirms="true"/>
<!--6.配置rabbitmq的模版,添加确认回调处理类:confirm- callback="msgSendConfirmCallback"-->
<!--配置rabbit模板接收发送消息-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
exchange="spring_topic_exchange"
message-converter="jsonMessageConverter"
confirm-callback="messageConfirm"/>
<!--确认机制的处理类-->
<bean id="messageConfirm" class="confirm.MessageConfirm"/>
- 确认机制的处理类
public class MessageConfirm implements RabbitTemplate.ConfirmCallback{
@Override
/**
* 参数一:消息相关的数据对象(封装了消息的唯一id)
* 参数二:消息确认是否成功
* 参数三:异常信息(出错咧才有的)
*/
public void confirm(CorrelationData correlationData, boolean b, String s) {
if (b){
System.out.println("消息确认发送成功");
}else {
System.out.println("消息确认发送失败");
System.out.println(s);
//如果本条消息一定要发送到队列中 例如下订单 ,我们可以采用补发
//1、采用递归(限制递归的次数)
//2、redis+定时任务(jdk的timer,或者定时任务框架)
}
}
}
- 发送消息测试
public class Sender {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-produce.xml");
//从容器中获取rabbit模板对象
RabbitTemplate template = context.getBean(RabbitTemplate.class);
//3、发消息
HashMap<String, String> map = new HashMap<String, String>();
map.put("name","皮皮潇123");
map.put("phone","123465");
// template.convertAndSend("msg.user",map);
//指定发送给的路由 为了报错 因为没有此路由lalala
template.convertAndSend("lalala","msg.user",map);
System.out.println("消息已发出");
context.close();
}
}
- 结果
消息确认发送失败
消费端限流
- 我们 Rabbitmq 服务器积压了成千上万条未处理的消息,然后随便打开一个消费者客户端,就会出现这样的情况: 巨量的消息瞬间全部喷涌推送过来,但是单个客户端无法同时处理这么多数据,就会被压垮崩溃
- 所以,当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,这是用户的行为,我们是无法约束的
- 我们应该对消费端限流,用于保持消费端的稳定
- RabbitMQ 提供了一种 Qos (Quality of Service,服务质量)服务质量保证功能
- 即在非自动确认消息的前提下,如果一定数目的消息未被确认前,不再进行消费新的消息
- 数据准备 生产者发送消息使堆积
for(int i = 1;i<=10;i++) {
rabbitTemplate.convertAndSend("msg.user", map);
System.out.println("消息已发出...");
}
- 堆积的消息
- 消费者进行限流处理
<!--配置监听-->
<!-- prefetch="3" 一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,一旦有 N 个消息还没有ack,
则该 consumer 将阻塞,直到消息被ack-->
<!--acknowledge="manual"手动确认-->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual">
<rabbit:listener ref="conListener" queue-names="test_spring_queue_1"/>
</rabbit:listener-container>
// AbstractAdaptableMessageListener用于在spring容器接收到消息后用于处理消息的抽象 基类
@Component
public class ConListener extends AbstractAdaptableMessageListener {
//jackson提供序列化和反序列化中使用最多的类 ,用来转换json
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override//处理消息类
public void onMessage(Message message, Channel channel) throws Exception {
//将message对象转换成json
try {
JsonNode jsonNode = MAPPER.readTree(message.getBody());
String name = jsonNode.get("name").asText();
String phone = jsonNode.get("phone").asText();
System.out.println("从队列中获取的name:"+name+" phone:"+phone);
//手动确认消息
/**
* 参数一:RabbitMQ向该channel投递的这条消息的唯一标识id,此id是单调递增正整数
* 参数二:为了减少网络流量,手动确认可以被批量处理,当该参数为true时,可以批量处理,可以一次性确认小于等于msgId值的所有消息
* 如果id为7,则可以一次性处理7条消息
*/
long msgId = message.getMessageProperties().getDeliveryTag();
channel.basicAck(msgId,true);
Thread.sleep(3000);
System.out.println("休息三秒后再继续接收消息");
} catch (IOException e) {
e.printStackTrace();
}
}
}
过期时间
Time To Live
:生存时间、还能活多久,单位毫秒- 在这个周期内,消息可以被消费者正常消费,超过这个时间,则自动删除(其实是被称为dead message并投入到死信队列,无法消费该消息)
- RabbitMQ可以对消息和队列设置TTL
- 通过队列设置,队列中所有消息都有相同的过期时间
- 对消息单独设置,每条消息的TTL可以不同(更颗粒化)
设置队列TTL
spring-rabbitmq-producer.xml
<!--2.重新配置一个队列,同时,对队列中的消息设置过期时间-->
<rabbit:queue name="test_spring_queue_ttl" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value-type="long" value="5000"></entry>
</rabbit:queue-arguments>
</rabbit:queue>
设置消息TTL
- 设置某条消息的ttl,只需要在创建发送消息时指定即可
public class Sender2 {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-produce.xml");
RabbitTemplate template = context.getBean(RabbitTemplate.class);
// 创建消息配置对象
MessageProperties messageProperties = new MessageProperties();
// 设置消息过期时间
messageProperties.setExpiration("6000");
// 创建消息
Message message = new Message("6秒后自动删除".getBytes(), messageProperties);
template.convertAndSend("msg.user", message);
System.out.println("消息已发出");
context.close();
}
}
- 注意:如果同时设置了queue和message的TTL值,则二者中较小的才会起作用
死信队列
DLX(Dead Letter Exchanges)
死信交换机/死信邮箱,当消息在队列中由于某些原因没有被及时消费而变成死信(dead message)后,这些消息就会被分发到DLX交换机中,而绑定DLX交换机的队列,称之为:“死信队列”- 消息没有被及时消费的原因
- 消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false
- 消息超时未消费
- 达到最大队列长度
spring-rabbitmq-producer-dlx.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--配置连接-->
<rabbit:connection-factory id="connectionFactory"
host="192.168.227.128"
port="5672"
username="ppx"
password="123456"
virtual-host="/lagou"
/>
<!--配置rabbitAdmin:主要用于在Java代码中对队列的管理,用来创建,绑定,删除队列与交换机,发送消息等-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--配置rabbit模板接收发送消息-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory" exchange="my_exchange"/>
<!--定义死信队列-->
<rabbit:queue name="dlx_queue"/>
<!--定义定向死信交换机-->
<rabbit:direct-exchange name="dlx_exchange">
<rabbit:bindings>
<rabbit:binding key="dlx_ttl" queue="dlx_queue"></rabbit:binding>
<rabbit:binding key="dlx_max" queue="dlx_queue"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--声明定向的此时消息的交换机-->
<rabbit:direct-exchange name="my_exchange">
<rabbit:bindings>
<rabbit:binding key="dlx_ttl" queue="test_ttl_queue"></rabbit:binding>
<rabbit:binding key="dlx_max" queue="test-max-queue"></rabbit:binding>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--声明 测试过期的消息队列-->
<rabbit:queue name="test_ttl_queue">
<rabbit:queue-arguments>
<!--设置队列的ttl-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
<!--如果消息超时 将消息投递给私信交换机-->
<entry key="x-dead-letter-exchange" value="dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--声明 测试超出长度的消息队列-->
<rabbit:queue name="test-max-queue">
<rabbit:queue-arguments>
<!--设置队列的ttl-->
<entry key="x-max-length" value-type="long" value="2"/>
<!--如果队列超过队列长度 将消息投递给私信交换机-->
<entry key="x-dead-letter-exchange" value="dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
</beans>
- 发消息测试
public class SenderDLx {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-rabbitmq-dlx.xml");
RabbitTemplate template = context.getBean(RabbitTemplate.class);
// template.convertAndSend("dlx_ttl","测试超时".getBytes());
/* template.convertAndSend("dlx_max","测试超长".getBytes());
template.convertAndSend("dlx_max","测试超长".getBytes());
template.convertAndSend("dlx_max","测试超长".getBytes());*/
template.convertAndSend("dlx_ttl","超时,关闭订单".getBytes());
context.close();
}
}
延迟队列
- 延迟队列:TTL + 死信队列的合体
- 死信队列只是一种特殊的队列,里面的消息仍然可以消费
- 在电商开发部分中,都会涉及到延时关闭订单,此时延迟队列正好可以解决这个问题
生产者
- 沿用上面死信队列案例的超时测试,超时时间改为订单关闭时间即可
消费者
<!-- 监听死信队列 -->
<rabbit:listener-container connection-factory="connectionFactory" prefetch="3" acknowledge="manual">
<rabbit:listener ref="consumerListener" queue-names="dlx_queue" />
</rabbit:listener-container>
RabbitMQ集群
- rabbitmq有3种模式,但集群模式是2种。详细如下:
- 单一模式:即单机情况不做集群,就单独运行一个rabbitmq而已。之前我们一直在用
- 普通模式:默认模式,以两个节点(A、B)为例来进行说明
- 当消息进入A节点的Queue后,consumer从B节点消费时,RabbitMQ会在A和B之间创建临时通道进行消息传输,把A中的消息实体取出并经过通过交给B发送给consumer
- 当A故障后,B就无法取到A节点中未消费的消息实体
- 如果做了消息持久化,那么得等A节点恢复,然后才可被消费
- 如果没有持久化的话,就会产生消息丢失的现象
- 镜像模式:非常经典的 mirror 镜像模式,保证 100% 数据不丢失
- 高可靠性解决方案,主要就是实现数据的同步,一般来讲是 2 - 3 个节点实现数据同步
- 对于 100% 数据可靠性解决方案,一般是采用 3 个节点
- 在实际工作中也是用得最多的,并且实现非常的简单,一般互联网大厂都会构建这种镜像集群模式
集群搭建
- 准备两台linux服务器,安装好RabbitMQ
- 集群步骤如下:
- 修改
/etc/hosts
映射文件 - 一号服务器
127.0.0.1 A localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 A localhost localhost.localdomain localhost6 localhost6.localdomain6 192.168.204.141 A 192.168.204.142 B
- 二号服务器
127.0.0.1 B localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 B localhost localhost.localdomain localhost6 localhost6.localdomain6 192.168.204.141 A 192.168.204.142 B
- 相互通信,cookie必须保持一致,同步 rabbitmq的cookie 文件:跨服务器拷贝
.erlang.cookie
(隐藏文件,使用 ls -all 显示)
scp /var/lib/rabbitmq/.erlang.cookie 192.168.204.142:/var/lib/rabbitmq
- 修改cookie文件,要重启服务器,
reboot
- 停止防火墙,启动rabbitmq服务
systemctl stop firewalld systemctl start rabbitmq-server
- 加入集群节点
[root@B ~]# rabbitmqctl stop_app [root@B ~]# rabbitmqctl join_cluster rabbit@A [root@B ~]# rabbitmqctl start_app
- 查看节点状态
[root@B ~]# rabbitmqctl cluster_status
- 查看管理端
- 搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的
- 所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)
[root@A ~]# rabbitmqctl add_user ppx 123123 [root@A ~]# rabbitmqctl set_user_tags ppx administrator [root@A ~]# rabbitmqctl set_permissions -p "/" ppx ".*" ".*" ".*"
- 注意:当节点脱离集群还原成单一结构后,交换机,队列和用户等数据 都会重新回来
- 修改
- 此时,集群搭建完毕,但是默认采用的模式“普通模式”,可靠性不高
镜像模式
- 将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态一致
- 语法:
set_policy {name} {pattern} {definition}
-
name
:策略名,可自定义 -
pattern
:队列的匹配模式(正则表达式)"^" 可以使用正则表达式
,比如"^queue_"
表示对队列名称以“queue_”开头的所有队列进行镜像,而"^"
表示匹配所有的队列
-
definition
:镜像定义,包括三个部分ha-mode
,ha-params
,ha-sync-mode
ha-mode
:(High Available,高可用)模式,指明镜像队列的模式,有效值all/exactly/nodes
,当前策略模式为 all,即复制到所有节点,包含新增节点
all:表示在集群中所有的节点上进行镜像 exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定 nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
ha-params
:ha-mode模式需要用到的参数ha-sync-mode
:进行队列中消息的同步方式,有效值为automatic
(自动)和manual
(手动)
-
- 语法:
[root@A ~]# rabbitmqctl set_policy xall "^" '{"ha-mode":"all"}
- 通过管理端设置镜像策略
HAProxy实现镜像队列的负载均衡
- 虽然我们在程序中访问A服务器,可以实现消息的同步,虽然在同步,但都是A服务器在接收消息,A太累
- 是否可以想Nginx一样,做负载均衡,A和B轮流接收消息,再镜像同步
HAProxy简介
- HA(High Available,高可用),Proxy(代理)
- HAProxy是一款提供高可用性,负载均衡,并且基于TCP和HTTP应用的代理软件
- HAProxy完全免费
- HAProxy可以支持数以万计的并发连接
- HAProxy可以简单又安全的整合进架构中,同时还保护web服务器不被暴露到网络上
HAProxy与Nginx
- OSI:(Open System Interconnection:开放式系统互联,是把网络通信的工作分为7层,分别是物理层,数据链路层,网络层,传输层,会话层,表示层和应用层)
- Nginx的优点
- 工作在OSI第7层,可以针对http应用做一些分流的策略
- Nginx对网络的依赖非常小,理论上能ping通就就能进行负载功能,屹立至今的绝对优势
- Nginx安装和配置比较简单,测试起来比较方便;
- Nginx不仅仅是一款优秀的负载均衡器/反向代理软件,它同时也是功能强大的Web应用服务器
- HAProxy的优点:
- 工作在网络4层和7层,支持TCP与Http协议,
- 它仅仅就只是一款负载均衡软件;单纯从效率上来讲HAProxy更会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的
- 支持8种负载均衡策略 ,支持心跳检测
- 性能上HA胜,功能性和便利性上Nginx胜
- 对于Http协议,Haproxy处理效率比Nginx高。所以,没有特殊要求的时候或者一般场景,建议使用Haproxy来做Http协议负载
- 但如果是Web应用,那么建议使用Nginx!
安装和配置
HAProxy下载:http://www.haproxy.org/download/1.8/src/haproxy-1.8.12.tar.gz
- 解压
[root@localhost opt]# tar -zxvf haproxy-1.8.12.tar.gz
- make时需要使用 TARGET 指定内核及版本
[root@localhost opt]# uname -r
3.10.0-514.6.2.el7.x86_64
- 进入目录,编译和安装
[root@localhost opt]# cd haproxy-1.8.12
[root@localhost haproxy-1.8.12]# make TARGET=linux2628
PREFIX=/usr/local/haproxy
[root@localhost haproxy-1.8.12]# make install PREFIX=/usr/local/haproxy
- 安装成功后,查看版本
[root@localhost haproxy-1.8.12]# /usr/local/haproxy/sbin/haproxy -v
- 配置启动文件,复制haproxy文件到
/usr/sbin
下 ,复制haproxy脚本,到/etc/init.d
下
[root@localhost haproxy-1.8.12]# cp /usr/local/haproxy/sbin/haproxy/usr/sbin/
[root@localhost haproxy-1.8.12]# cp ./examples/haproxy.init /etc/init.d/haproxy
[root@localhost haproxy-1.8.12]# chmod 755 /etc/init.d/haproxy
- 创建系统账号
[root@localhost haproxy-1.8.12]# useradd -r haproxy
haproxy.cfg
配置文件需要自行创建
[root@localhost haproxy-1.8.12]# mkdir /etc/haproxy
[root@localhost haproxy-1.8.12]# vim /etc/haproxy/haproxy.cfg
- 添加配置信息到haproxy.cfg
# 全局配置
global
# 设置日志
log 127.0.0.1 local0 info
# 当前工作目录
chroot /usr/local/haproxy
# 用户与用户组
user haproxy
group haproxy
# 运行进程 ID
uid 99
gid 99
# 守护进程启动
daemon
# 最大连接数
maxconn 4096
# 默认配置
defaults
# 应用全局的日志配置
log global
# 默认的模式 mode {tcp|http|health},TCP 是 4 层,HTTP 是 7 层,health 只返回 OK
mode tcp
# 日志类别 tcplog
option tcplog
# 不记录健康检查日志信息
option dontlognull
# 3 次失败则认为服务不可用
retries 3
# 每个进程可用的最大连接数
maxconn 2000
# 连接超时
timeout connect 5s
# 客户端超时 30 秒,ha 就会发起重新连接
timeout client 30s
# 服务端超时 15 秒,ha 就会发起重新连接
timeout server 15s
# 绑定配置
listen rabbitmq_cluster
bind 192.168.227.130:5672
# 配置 TCP 模式
mode tcp
# 简单的轮询
balance roundrobin
# RabbitMQ 集群节点配置,每隔 5 秒对 mq 集群做检查,2 次正确证明服务可用,3 次失败证明服务不可用
server A 192.168.227.128:5672 check inter 5000 rise 2 fall 3
server B 192.168.227.129:5672 check inter 5000 rise 2 fall 3
# haproxy 监控页面地址
listen monitor
bind 192.168.227.130:8100
mode http
option httplog
stats enable
# 监控页面地址 http://192.168.227.130:8100/monitor
stats uri /monitor
stats refresh 5s
- 启动HAProxy
[root@localhost haproxy]# service haproxy start
- 访问监控中心:http://192.168.227.130:8100/monitor
- 记得关闭防火墙:
systemctl stop firewalld
- 项目发消息,只需要将服务器地址修改为143即可(haproxy),其余不变
- 所有的请求都会交给HAProxy,其负载均衡给每个rabbitmq服务器
KeepAlived搭建高可用的HAProxy集群
- 现在的最后一个问题暴露出来了,如果HAProxy服务器宕机,rabbitmq服务器就不可用了。所以我们需要对HAProxy也要做高可用的集群
KeepAlived概述
- Keepalived是Linux下一个轻量级别的高可用热备解决方案
- Keepalived的作用是检测服务器的状态,它根据TCP/IP参考模型的第三、第四层、第五层交换机制检测每个服务节点的状态,如果有一台web服务器宕机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,同时使用其他服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。
- keepalived基于vrrp(
Virtual Router Redundancy Protocol
,虚拟路由冗余协议)协议,vrrp它是一种主备(主机和备用机)模式的协议,通过VRRP可以在网络发生故障时透明的进行设备切换而不影响主机之间的数据通信 - 两台主机之间生成一个虚拟的ip,我们称漂移ip,漂移ip由主服务器承担,一但主服务器宕机,备份服务器就会抢夺漂移ip,继续工作,有效的解决了群集中的单点故障
- 说白了,将多台路由器设备虚拟成一个设备,对外提供统一ip
安装KeepAlived
- 修改hosts文件的地址映射
ip | 用途 | 主机名 |
---|---|---|
192.168.227.130 | KeepAlived HAProxy | C |
192.168.227.131 | KeepAlived HAProxy | D |
- 安装 keepalived
[root@C ~]# yum install -y keepalived
- 修改配置文件(内容大改,不如删掉,重新创建)
[root@C ~]# rm -rf /etc/keepalived/keepalived.conf
[root@C ~]# vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
# 非常重要,标识本机的 hostname
router_id C
}
vrrp_script chk_haproxy {
# 执行的脚本位置
script "/etc/keepalived/haproxy_check.sh"
# 检测时间间隔
interval 2
# 如果条件成立则权重减 20
weight -20
}
vrrp_instance VI_1 {
# 非常重要,标识主机,备用机 131 改为 BACKUP
state MASTER
# 非常重要,网卡名(ifconfig 查看)
interface ens33
# 非常重要,自定义,虚拟路由 ID 号(主备节点要相同)
virtual_router_id 66
# 优先级(0-254),一般主机的大于备机
priority 100
# 主备信息发送间隔,两个节点必须一致,默认 1 秒
advert_int 1
# 认证匹配,设置认证类型和密码,MASTER 和 BACKUP 必须使用相同的密码才能正常通信
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
# 检查 haproxy 健康状况的脚本
chk_haproxy
}
# 简称 “VIP”
virtual_ipaddress {
# 非常重要,虚拟 ip,可以指定多个,以后连接 mq 就用这个虚拟ip
192.168.227.66/24
}
}
# 虚拟 ip 的详细配置
virtual_server 192.168.227.66 5672 {
# 健康检查间隔,单位为秒
delay_loop 6
# lvs 调度算法 rr|wrr|lc|wlc|lblc|sh|dh
lb_algo rr
# 负载均衡转发规则。一般包括 DR, NAT, TUN 3 种
lb_kind NAT
# 转发协议,有 TCP 和 UDP 两种,一般用 TCP
protocol TCP
# 本机的真实 ip
real_server 192.168.227.130 5672 {
# 默认为 1, 失效为 0
weight 1
}
}
- 创建执行脚本
/etc/keepalived/haproxy_check.sh
#!/bin/bash
COUNT=`ps -C haproxy --no-header |wc -l`
if [ $COUNT -eq 0 ];then
/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
sleep 2
if [ `ps -C haproxy --no-header |wc -l` -eq 0 ];then
killall keepalived
fi
fi
-
Keepalived
组之间的心跳检查并不能察觉到HAproxy
负载是否正常,所以需要使用此脚本。在Keepalived
主机上,开启此脚本检测HAproxy
是否正常工作,如正常工作,记录日志。如进程不存在,则尝试重启HAproxy
,2 秒后检测,如果还没有,则关掉主机的Keepalived
,此时备Keepalived
检测到主Keepalived
挂掉,接管 VIP,继续服务。 -
授权,否则不能执行
[root@C etc]# chmod +x /etc/keepalived/haproxy_check.sh
- 启动keepalived(两台都启动)
[root@C etc]# systemctl stop firewalld
[root@C etc]# service keepalived start | stop | status | restart
- 查看状态
[root@C etc]# ps -ef | grep haproxy
[root@C etc]# ps -ef | grep keepalived
- 查看ip情况 ip addr 或 ip a
[root@C etc]# ip a
启动 keepalived 前的情况:
[root@C keepalived]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 00:0c:29:ac:93:50 brd ff:ff:ff:ff:ff:ff
inet 192.168.227.130/24 brd 192.168.186.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:feac:9350/64 scope link
valid_lft forever preferred_lft forever
启动后的情况
[root@C keepalived]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 00:0c:29:ac:93:50 brd ff:ff:ff:ff:ff:ff
inet 192.168.227.130/24 brd 192.168.227.255 scope global ens33
valid_lft forever preferred_lft forever
inet 192.168.227.66/24 scope global secondary ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:feac:9350/64 scope link
valid_lft forever preferred_lft forever
- 可以看到 ens33 网卡还多绑定了一个 IP 地址。
- 此时,安装完毕,按照上面的步骤就可以安装第二台了(服务器hostname和ip注意要修改)
- 测试vip+端口是否提供服务(在128,A服务器上测试)
[root@A ~]# curl 192.168.227.66:5672
AMQP ## 正常提供AMQP服务,表示通过vip访问mq服务正常
- 测试项目发消息
消费者或生产者 -- 漂移 IP 66 --> KeepAlived 服务 --> [HAProxy 服务器C 130, HAProxy 服务器D 131]
HAProxy 服务器C 130 -- 负载均衡 --> [MQ 服务器A 128, MQ 服务器B 129]
HAProxy 服务器D 130 -- 负载均衡 --> [MQ 服务器A 128, MQ 服务器B 129]
- 测试单个 RabbitMQ 服务器:将服务器地址修改为
192.168.227.128
,其余不变。 - 测试 HAProxy 实现多个 RabbitMQ 服务器负载均衡:将服务器地址修改为
192.168.227.130
,其余不变。 - 测试 KeepAlived 实现的高可用的 HAProxy 集群:将服务器地址修改为 KeepAlived 的虚拟 IP
192.168.227.66
,其余不变。
测试 ip 漂移的规则
- 查看虚拟ip
ip addr
或ip a
- 目前,C节点是主机,所以虚拟ip在C节点
- 停止C的keepalived,虚拟ip漂移到D节点
- 重新启动C节点keepalived,虚拟ip依旧在D节点,并不会由于C的回归而回归
- 停止D的keepalived,虚拟ip再漂移回C节点