目录
15.3Haproxy+Keepalive实现高可用负载均衡
站B学习网址:01-课程介绍_哔哩哔哩_bilibili
1、RabbitMQ和Erlang下载(windows)
- 安装RabbitMQ之前必须先安装Erlang环境
- 安装前先参照rabbitmq官网给的RabbitMQ和Erlang版本对应关系下载对应版本的软件
RabbitMQ官网下载址:Installing on Windows — RabbitMQ
Erlang官网下载地址:Downloads - Erlang/OTP
1.1Erlang安装
点击下载好的.exe文件进行傻瓜式安装(一直next)即可
配置Erlang环境变量
打开命令窗口,输入erl
验证环境是否配置成功(出现以下版本号即bingo,显示不是内部命令则环境变量配置成功)
1.2RabbitMQ安装
和安装Erlang一样,点击exe文件进行安装配置环境变量
1.3安装管理工具RabbitMQ-Plugins
进入sbin文件下,打开命令窗口输入
rabbitmq-plugins enable rabbitmq_management
如果不是显示上图中的信息而是出现以下表示错误
Please either set ERLANG_HOME to point to your Erlang installation or place the
RabbitMQ server distribution in the Erlang lib folder
只要之前步骤都成功了,重启电脑,重新输入命令rabbitmq-plugins enable rabbitmq_management安装即可
安装好管理工具后输入rabbitmq-server.bat
启动rabbitMQ
最后输入http://localhost:15672
(默认账号:guest,密码:guest)就能进入RabbitMQ管理界面
2、MQ的基本概念
2.1MQ概述
- MQ,消息队列,存储消息的中间件
- 分布式系统通信两种方式:直接远程调用和借助第三方完成间接通信
- 发送方称为生产者,接收方称为消费者
2.2MQ优势和劣势
2.3常见的mq产品
2.4RabbitMQ简介
AMQP,即Advanced Message Queuing Protocal(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP规范发布。类比HTTP。
2007年,Rabbit技术公司基于AMQP标准开发的RabbitMQ1.0发布。RabbitMQ采用Erlang语言开发。Erlang语言由Ericson设计,专门为开发高并发和分布式系统的一种语言,在电脑领域使用广泛。
RabbitMQ基础架构如下图:
2.5JMS
- JMS即JAVA消费服务(JavaMwssage Service)应用程序接口,是一个java平台中关于面向消息中间件的API
- JMS是JAVAEE规范中的一种,类比JDBC
- 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ官方没有提供JMS的实现包,但是开源社区有
小结:
- RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品。
- RabbitMQ提供了6种工作模式,我们学习5种。
- AMQP是协议,类比HTTP。
- JMS是API规范接口,类比JDBC。
3、RabbitMQ快速入门
3.1入门程序
需求:使用简单模式完成消息传递
步骤:
- 创建工程(生成者、消费者)
- 分别添加依赖
- 编写生产者发送消息
- 编写消费者消费消息
添加依赖:
生产者发送消息:
消费者消费消息:
4、RabbitMQ的工作模式
4.1Work queues工作队列模式
- Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
- 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
小结:
- 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
- Work Queues对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。例如:短信服务部署多个,只需要有一个节点成功发送即可。
4.2Pub/Sub订阅模式
在订阅模型中,多了一个Exchange角色,而且过程略有变化;
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发送给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列 ,接收消息,缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型
1、Fanout:广播,将消息交给所有绑定到交换机的队列
2、Direct:定向,把消息交给符合指定routing key 的队列
3、Topic:通配符,把消息交给符合routing pattern(路由模式)的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失
Fanout生产者代码:
Fanout消费者代码:
4.3Routing路由模式
1、模式说明:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的RoutingKey进行判断,只有队列的RoutingKey与消息的RoutingKey完全一致,才会接收到消息
Direct生产者代码:
Direct消费者代码不变,只需修改两个消费者消费的队列名称即可。
4.4Topics通配符模式
Topic生产者代码:
4.5工作模式总结
5、Spring整合RabbitMQ
需求:使用Spring整合RabbitMQ
生产者发送消息代码:
消费者接收消息代码:
6、SpringBoot整合RabbitMQ
配置文件:
启动类:
配置类:
消息发送:
yml配置文件:
监听类:
小结:
- SpringBoot提供了快速整合RabbitMQ的方式
- 基本信息在yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置
- 生产端直接注入RabbitTemplate完成消息发送
- 消息端直接使用@RabbitListener完成消息接收
7、消息应答
7.1概念
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅完成了部分突然它挂掉了,会发生什么情况。Rabbitmq一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给消费者的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除。
7.2自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
7.3手动应答消息的方法
A、Channel,basicAck(用于肯定确认)
RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了
B、Channel,basicNack(用于否定确认)
C、Channel,basicReject(用于否定确认)
与Channel.basicNack相比少一个参数
不处理该消息了直接拒绝,可以将其丢弃了
7.4Multiple的解释
手动应答的好处是可以批量应答并且减少网络的拥堵
channel.basicAck(deliveryTag,true);
multiple的true和false代表不同的意思,true代表批量应答channel上为应答的消息,比如说channel上有传送tag的消息5,6,7,8当前tag是8那么此时5-8的这些还未应答的消息都会被确认收到消息应答。false同上面相比,只会应答tag=8的消息5,6,7在这三个消息依然不会被确认收到消息。
因此批量应答可能会应答一些没有处理完的消息,导致消息丢失,一般不建议采用批量应答机制。设置为false处理完成一个应答一个。
7.5消息重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,Rabbitmq将了解到消息未完全处理,并将其重新排队。如果此时其他消费者可以处理,他将很快1将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
8、RabbitMQ持久化
8.1概念
刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当RabbitMQ服务停掉以后消息生产者发送过来的消息不丢失。默认情况下RabbitMQ退出或由于某种原因崩溃时,它忽略对列和消息,除非告知他不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
8.2队列如何实现持久化
之前我们创建的队列都是非持久化的,rabbitmq如果重庆的话,该队列会被删除,
如果要队列实现持久化,需要在声明队列的时候把durable参数设置为持久化。
//让消息队列持久化
boolean durable=true;
channel.queueDeclare(ACK_QUEUE_NAME,durable,false,false,null);
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。队列持久化之后即使重启rabbitmq队列也依然存在。
8.3消息实现持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN添加这个属性。
channel.basicPublish("",TASK_QUEUE,nulll,message.getBytes("UTF-8");
改为一下代码:
channel.basicPublish("",TASk_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8");
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点,此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这里已经绰绰有余了。如果需要更强有力的持久化策略,参考后边课件发布确认章节。
8.4不公平分发
在最开始的时候我们学习到的RabbitMQ分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者1处理任务的速度非常快,而另一个消费者2处理速度确很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在某种情况下其实就不太好,但是RabbitMQ并不知道这种情况它依然很公平的进行分发。为了避免这种情况,我们可以设置参数channel.basicQos(1);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
8.5预取值
该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认。预取值的设置与不公平分发相似。如下:
//设置其中一个消费者信道的预取值为5,表示该信道最大容量为5条消息
int prefetchCount = 5;
channel.basicQos(prefetchCount);
9、发布确认
9.1发布确认原理
生产者将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一id),这就使得生产者知道消息已经正确到达目的的队列了。如果消息的队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,2.表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认后,生产者应用使可以通过回调方法来处理该确认消息。如果Rabbitmq因为自身内部错误导致消息丢失,就会发送一条nack消息,生.产者应用程序同样可以在回调方法中处理该nack消息。
9.2发布确认策略
9.2.1单个确认发布
这是一个简单的确认方式,它是一种同步确认发布方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)
这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别慢,因为如果没有1确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用来说这可能已经足够了。
9.2.2批量确认发布
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大的提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
9.2.3异步确认发布
异步确认虽然编程逻辑比以上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面让我们详细讲解异步确认是怎么实现的。
9.2.4如何处理异步未确认消息
最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的比如说ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递。
10、交换机
10.1Exchanges
交换机作用:如果只有生产者、队列、消费者简单模式组成,由于一个队列的消息只能被一个消费者消费,即使设置两个或多个消费者,这种模式下也是属于竞争模式的。因此当我们的需求是需要一个消息可以被多个消费者处理时,就需要使用交换机,消息从生产者到达交换机,再由交换机发送到绑定的多个队列中,队列中的消息被不同消费者使用,最终达到相同的消息可以被多个不同的消费者处理的目录。
10.1.1Exchanges概念
RabbitMQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递到了那些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将他们推入队列。交换机必须确切知道如何处理收到的消息。是应该把收到的消息放到特定队列还是说把它们放到许多队列中还是说应该丢弃它们,这就由交换机的类型来决定。
10.1.2Exchanges的类型
总共有一下几种类型:
直接(direct),主题(topit),标题(headers),扇出(fanout)
10.1.3无名exchange
在本教程的前面部分我们对exchange一无所知,但是仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换机,我们通过空字符串("")进行标识。
channel.basicPublish("","hello",null,message.getBytes());
第一个参数是交换机的名字。空字符串表示默认或者无名交换机:消息能够路由发送到队列中其实是由routingKey(bindingkey)绑定key指定的,如果它存在的话
10.2临时队列
之前的章节我们使用的是具有特定名称的队列(还记得hello和ack_queue吗?)。队列的名称对我们来说至关重要-我们需要指定我们的消费者去消费那些队列的消息。
每当我们连接到Rabbit时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方法如下:
String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样:
10.3绑定(bindings)
什么是binding呢,binding其实是exchange和queue之间的桥梁,它告诉我们exchange和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是X与Q1和Q2进行了绑定
10.4Fanout
10.4.1Fanout介绍
Fanout这种类型非常简单,正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。系统中默认有些exchange类型
10.5Direct exchange
10.5.1回顾
在上一节中,我们构建了一个简单的日志记录系统。我们能够向许多接收者广播日志消息。c在本节我们将向其中添加一些特别的功能-比方说我们只让某个消费者订阅发布的部分消息。例如我们只把严重错误消息定向存储到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有的日志消息。
我们再次来回顾一下什么是bindings,绑定是交换机和队列之间的桥梁关系,也可以这么理解:队列只对它绑定的交换机的消息感兴趣。绑定用参数:routingkey来表示也可称该参数为binding key,创建绑定我们用代码:channel.queueBind(queueName,EXCHANGE_NAME,"routingKey");绑定之后的意义由其交换类型决定。
10.5.2Direct exchange介绍
上一节中我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希望将日志消息写入磁盘的程序仅接收严重错误(erros),而不存储那些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用direct这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingkey队列中去。
在上面这张图中,我们可以看到X绑定了两个队列,绑定类型是direct。队列Q1绑定键为orange,队列Q2绑定键有两个:一个绑定键为black,另一个绑定键为green.
在这种绑定情况下,生产者发布消息到exchange上,绑定键为orange的消息会被发布到队列Q1。绑定键为blackgreen和black的消息会被发布到队列Q2,其他消息类型的消息将会被丢弃。
10.5.3多重绑定
当然如果exchange的绑定类型是direct,但是它绑定的多个队列的key如果都相同,在这种情况下虽然绑定类型是direct但是它表现的就和fanout有点类似了,就跟广播差不多,如上图所示。
10.6Topics
10.6.1之前类型的问题
在上一个小节中,我们改进了日志记录系统。我们没有使用只能进行随意广播的fanout交换机,而是使用了direct交换机,从而能实现有选择性的接收日志。
尽管使用direct交换机改进了我们的系统,但是它仍然存在局限性-比方说我们想接受的日志类型有info.base和info.advantage,某个队列只想接收info.base的消息,那这个时候direct就办不到了。这个时候只能使用topic类型。
10.6.2Topic的要求
发送到类型是topic交换机的消息的routing_key不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse","nyse.vmw","quick.orange.rabbit",这种类型的。当然这个单词列表最多不能超过255个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以代替零个或多个单词
10.6.3Topic匹配案例
下图绑定关系如下:
Q1绑定的是:中间带orange带3个单词的字符串(*.orange.*)
Q2绑定的是:第一个单词是lazy的多个单词(lazy.#)、最后一个是rabbit的3个单词(*.*.rabbit)
上面是一个绑定关系图,我们来看看他们之间数据接收情况是怎么样的
当队列绑定关系是下列这种情况时需要引起注意
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout了
如果队列绑定键中没有*和#出现,那么该队列绑定类型就是direct了
11、死信队列
11.1死信的概念
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue里面取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动消失。
11.2死信的来源
- 消息TTL(存活时间)过期
- 队列达到最大长度(队列满了,无法再添加数据到mq中)
- 消息被拒绝(basic.reject或basic.nack)并且requeue=false(不放回队列中)
12、延迟队列
12.1延迟队列概念
延迟队列,延迟队列内部是有序的,最重要的特性就体现在它的延时属性上,演示队列中的元素是希望在指定的时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。(延迟队列属于死信队列中消息过期中的一种队列)
12.2延迟队列使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在10天内都没有上传过商品,则自动发送消息提醒
- 用户注册成功后,如果三天内没有登录则进行短信提醒
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员
- 预定会议后,需要在预定的时间前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个时间发生之后或者之前的指定时间完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算:这样的需求如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但是对于数据量比较大,并且时效性较强的场景,如:"订单上分钟内未支付则关闭”,短期内未支付的订单数据可能会很多,活动期间可能会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大的压力,无法满足业务要求而且性能底下。
12.3延迟队列的实现方法
方法1:声明普通队列时在最后一个参数中传入队列的延时时间,这样可以设置多个不同的队列声明不同的延时时间来处理消息,在队列中的消息达到延时时间将送到延时交换机与延时队列最后被消费者处理延时消息。此方法的缺点是:如果大量消息的延时时间都是不同的,那么需要创建许多队列来设置不同的延时时间进行处理,因此为了优化有了第二种方法。
方法2:延时时间不在队列声明中进行设置,而是放在发消息时设置当前的消息过期时间,消息在正常队列中存放知道到达过期时间进入延时交换机延时队列最后被处理。但是缺点是如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡”,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先执行,而是等第一个消息的延时时间到了执行再检查第二个消息,此时第二个消息延时时间早已到达就直接执行第二个消息。
方法3:为了解决方法2中存在的问题,使消息在设置的TTL时间到达时及时死亡达到延时交换机被处理,完成一个通用的延时队列,解决方法是使用插件实现延迟队列。
如上图所示,方法1和方法2是基于队列的延迟,方法三使用插件实现的延迟队列是基于交换机的延迟。
12.4基于插件的延迟队列实现代码
步骤1:官网下载安装延迟队列插件
步骤2:编写配置类:
步骤3:编写生产者
步骤4:编写消费者
12.5总结
延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正常处理的消息不会被丢弃。另外,通过RabbitMQ集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其他选择,比如利用java和DelayQueue,利用Redis的zset,利用Quartz或者kafka的时间轮,这些方式各有特点,看需要使用的场景
13、发布确认高级
在生产环境中由于一些不明原因,导致rabbitmq重启,在RabbitMQ重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复,于是,我们开始思考,如何才能进行Rabbitmq的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ集群不可用的时候,无法投递消息如何处理呢:
13.1发布确认springboot版本
13.1.1确认机制方案
13.1.2配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
1、NONE:禁用发布确认模式,是默认值
2、CORRELATED:发布消息成功到交换器后会触发回调方法
3、SIMPLE:经测试有两种效果,其一效果和CORRELATED值一样会出发回调方法,
其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker
13.1.3配置类
13.1.4生产者
13.1.5消费者
13.1.6回调接口
13.2回退消息
13.2.1Mandatory参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的,那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
13.2.2回退接口
13.3备份交换机
有了mandatory参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题,什么是备份交换机呢?备份交换机可以理解为RabbitMQ中交换机的“备胎”,当我们为一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由的消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行检测和报警。
13.3.1代码架构图
13.3.2修改配置类
定义备份交换机、备份队列、备份警告队列名字
声明备份交换机、备份队列、备份警告队列
将备份交换机与备份队列绑定、备份交换机与警告队列绑定
修改确认交换机的定义,当消息无法投递给确认交换机时发送给备份交换机
13.3.3编写报警消费者
13.3.4结果分析
mandatory参数(发布确认机制,交换机不能接收的消息由回调函数回退给生产者)与备份交换机可以一起使用的使用,如果两者同时开启,消息究竟何去何从?谁优先级高,经过测试是备份交换机优先级高。
14、RabbitMQ其它知识点
14.1幂等性
14.1.1概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣掉了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络中断或者异常等等。
14.1.2消息重复消费
消费者在消费MQ中的消息时,MQ已把消息发送给消费者,消费者在给MQ返回ack时网络中断,故MQ未收到确认消息,该消息会重新发送给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
14.1.3解决思路
MQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费者消费MQ中的消息也可利用MQ的该id来判断,或者可按自己的规则生成一个全局的唯一id,每次消费消息时该id先判断该消息是否已经消费过。
14.1.4消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发送了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a、唯一的ID+指纹码机制,利用数据库主键去重,b、利用redis的原子性去实现。
14.1.5唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
14.1.6Redis原子性
利用redis执行setnx命令,天然具有幂等性,从而实现不重复消费。
14.2优先级队列
14.2.1使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一跳短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理所当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用redis来存放的定时轮询,大家都知道redis只能用List做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用RabbitMQ进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
14.2.2如何添加
方法一:控制台页面添加
方法二:代码中实现优先级
先在队列定义中添加优先级
Map<String,Object> params = new HashMap();
params.put("x-max-priority",10);
channel.queueDeclare("hello",true,false,false,params);
然后在消息中代码添加优先级
//发消息设置的优先级(例如本次的5)要在队列定义优先级以内(例如上面设置的10以内)
AMQP。BasicProperties properties = new AMQP.BasicProperties.builder().priority(5).build();
注意事项:要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息全部已经发送到队列中才去消费,因为这样才有机会对消息进行排序完再消费从而实现优先级
14.2.3代码实现
上面代码只需修改一下几点即可:
14.3惰性队列
14.3.1使用场景
RabbitMQ从3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持长久的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线,宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
14.3.2两种模式
队列具备两种模式;default和lazy。默认的为default模式,在3.6.0之前的版本无需做任何变更,lazy模式即为惰性队列模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方法具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过"x-queue-mode"参数来设置队列的模式,取值为"default"和"lazy"。下面示例中演示了一个惰性队列的声明细节:
Map<String,Object> args = new HashMap<String,Object>();
args.put("x-queue-mode","lazy");
channel.queueDeclare("myqueue",false,false,false,args);
14.3.3内存开销对比
在发送一百万条消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用1.5MB
15、RabbitMQ集群
15.1clustering
15.1.1使用集群的原因
最开始我们介绍了如何安装及3运行RabbitMQ服务,不过这些是单机版的,无法满足目前真实应用的要求。如果RabbitMQ服务器遇到内存崩溃,机器掉电或者主板故障等情况,该怎么办?单台RabbitMQ服务器可以满足每秒1000条消息的吞吐量,那么如果应用需要RabbitMQ服务满足每秒10万条消息的吞吐量呢?购买昂贵的服务器来增强单机RabbitMQ服务的性能显得捉襟见肘,搭建一个RabbitMQ集群才是解决实际问题的关键。
15.1.2搭建步骤
1、修改3台机器的主机名称分别为node1、node2、node3
vim /etc/hostname
修改完进行重启生效
2、配置各个节点的hosts文件,让各个节点都能互相识别对方
vim /etc/hosts
10.211.55.74 node1
10.211.55.75 node2
10.211.55.76 node3
3、以确保各个节点的cookie文件使用的是同一个值
在node1上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
4、重启RabbitMQ服务,顺带启动Erlang虚拟机和RabbitMQ应用服务(在三台节点上
分别执行以下命令)
rabbitmq-server -detached
5、在节点2执行
rabbitmqctl stop_app
(rabbitmqctl stop会将Erlang虚拟机关闭,rabbitmqctl stop_app只关闭RabbitMQ服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app(只启动应用服务)
6、在节点3执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
7、集群状态
rabbitmqctl cluster_status
8、需要重新设置用户
创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
9、解除集群节点(node2和node3机器分别执行)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2(node1机器上运行)
15.2镜像队列
15.2.1使用镜像的原因
如果RabbitMQ集群中只有一个Broker节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,但是这样任然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘并执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过publisherconfirm机制能够确保客户端知道那些消息已经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动的切换到镜像中另一个节点上以保证服务的可用性。
15.2.2搭建步骤
1、启动三台集群节点
2、随便找一个节点添加policy
3、在node1上创建一个队列发送一条消息,队列存在镜像队列
4、停掉node1之后发现node2成为镜像队列
5、就算整个集群只剩一台机器了,依然能消费队列里面的消息
说明队列里面的消息被镜像队列传递到对应机器里面了
15.3Haproxy+Keepalive实现高可用负载均衡
15.3.1整体架构图
15.3.2Haproxy实现负载均衡
HAProxy提供高可用性、负载均衡及基于TCPHTTP应用的代理,支持虚拟机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用,HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。
扩展nginx,lvs,haproxy之间的区别:http://www.ha97.com/5646.html
15.3.3搭建步骤
15.4Federation Exchange
15.4.1使用它的原因
(broker北京),(broker深圳)彼此之间的距离甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client北京)需要连接(broker北京),向其中的交换机exchangeA发送消息,此时的网络延迟很小,(Client北京)可以迅速将消息发送至exchangeA中,就算在开启了publisherconfirm机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client深圳)将发送消息至exchangeA会经历一定的延迟,尤其是在开启了publisherconfirm机制或者事务机制的情况下,(Client深圳)会等待很长的延迟时间来接收(broker北京)的确认消息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。
将业务(Client深圳)部署到北京的机房可以解决这个问题,但是如果(Client深圳)调用的另些服务部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用Federation插件可以很好地解决这个问题。
15.4.2搭建步骤
1、需要保证每台节点单独运行
2、在每台机器上开启federation相关插件
rabbitmq-plugins enable rabbitmq_federation:
rabbitmq-plugins enable rabbitmq_federation_management
3、原理图(先运行consumer在node2创建fed_exchange)
代码实现:声明联邦交换机和联邦队列,绑定联邦交换机与队列
4、在downstream(node2)配置upstream(node1)
5、添加policy
14.5Federation Queue
14.5.1使用它的1原因
联邦队列可以在多个Broker节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息的需求。
14.5.2搭建步骤
1、原理图
2、添加upstream(同上联邦交换机步骤)
3、添加policy
14.6Shovel
14.6.1使用它的原因
Federation具备的数据转发功能类似,Shovel够可靠、持续的从一个Broker中的队列(作为源端,即source)拉取数据并转发至另一个Broker中的交换器(作为目的端,即destination)。作为源端的队列和作为目的端的交换机可以同时位于同一个Broker,也可以位于不同的Broker上。Shovel可以翻译为“铲子”,是一种比较形象的比喻,这个“铲子”可以将消息从一方“铲子”另一方。Shovel行为就像优秀的客户端应用程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。
14.6.2搭建步骤
1、开启插件(需要的机器都开启)
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugin enable rabbitmq_shovel_management
2、原理图(在源头发送的消息直接会进入到目的地队列)
3、添加shovel源和目的地