什么是 MQ
MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是
message 而已。
还是一种跨进程的通信机制,用于上下游传递消息。
在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。
使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
为什么要用 MQ
1.流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正
常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限
制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分
散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体
验要好。
2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合
调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于
消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在
这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流
系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3.异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可
以执行完。
以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。
或者 A 提供一个 callback api,B 执行完之后调用 api 通知 A 服务。
这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,
A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。
这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。
同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息
MQ有什么缺点?
系统可用性降低
系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人 ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。(可以利用集群解决)
系统复杂性提高
硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
一致性问题(保证消息不丢失)
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的。
MQ 的分类
ActiveMQ
优点:单机吞吐量万级,时效性 ms 级,可用性高,基于主从架构实现高可用性,消息可靠性较
低的概率丢失数据
缺点:官方社区现在对 ActiveMQ 5.x 维护越来越少,高吞吐量场景较少使用。
Kafka
大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开 Kafka,这款为大数据而生的消息中间件,
以其百万级 TPS 的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。目前已经被 LinkedIn,Uber, Twitter, Netflix 等大公司所采纳。
优点: 性能卓越,单机写入 TPS 约在百万条/秒,最大的优点,就是吞吐量高。时效性 ms 级可用性非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采用 Pull 方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方
Kafka Web 管理界面 Kafka-Manager;在日志领域比较成熟,被多家公司和多个开源项目使用;功能支持:功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
缺点:Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,社区更新较慢;
RocketMQ
RocketMQ 出自阿里巴巴的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一
些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog 分发等场景。
优点:单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到 0 丢失,MQ 功能较为完善,还是分
布式的,扩展性好,支持 10 亿级别的消息堆积,不会因为堆积导致性能下降,源码是 java 我们可以自己阅
读源码,定制自己公司的 MQ
缺点:支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟;社区活跃度一般,没有在 MQ
核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码
RabbitMQ
2007 年发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最
主流的消息中间件之一。
优点:由于 erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ 功能比较完备,健壮、稳定、易
用、跨平台、支持多种语言 如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持 AJAX 文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高
缺点:商业版需要收费,学习成本较高
MQ 的选择
Kafka
Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集
和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka 了。
RocketMQ
天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削
峰,在大量交易涌入时,后端可能无法及时处理的情况。RocketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。
RabbitMQ
结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分
方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。
RabbitMQ 的概念
RabbitMQ 是一个消息中间件:它接受并转发消息。
你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。
RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
四大核心概念
生产者 Provider
产生数据发送消息的程序是生产者
消费者 Consumer
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费
者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
交换机 Broker
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息
推送到队列中。
交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列 Queue
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存
储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。
许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
RabbitMQ基本概念
AMQP协议
AMQP协议,所谓的高级消息队列协议,可以把它理解成一种公认的协议规范,就像http协议一样,只是这个AMQP协议针对的是消息队列。这个协议使得遵从了它的规范的客户端应用和消息中间件服务器的全功能互操作成为可能。
Broker
接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
Virtual host
出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection
publisher/consumer 和 broker 之间的 TCP 连接
Channel
如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP
Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的
Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange
message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue
消息最终被送到这里等待 consumer 取走
Binding
exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保
存到 exchange 中的查询表中,用于 message 的分发依据
vhost 是什么? 起什么作用?
每一个rabbitmq服务器都能创建虚拟的消息服务器,我们称之为虚拟主机(virtual host)。简称vhost
特性:
每一个vhost本质上是一个小型的独立的rabbitmq服务器,拥有自己独立的完整的一套队列、绑定关系、交换器等。同一个服务器上的多个vhost是完全隔离的。队列及交换器等不互通。
所以一个broker可以开设多个vhost,用于不同用户的权限分离
如何创建vhost?
1)通过前台页面的admin中创建
2)使用rabbitmqctl add_vhost vhost名称 命令
如何删除vhost?
1)前台删除
2)rabbitmqctl delete_vhost vhost_name
Rabbitmq里的交换机类型有哪些?
1、什么是Exchange
在RabbitMQ中,生产者发送消息不会直接将消息投递到队列中,而是先将消息投递到交换机中, 在由交换机转发到具体的队列, 队列再将消息以推送或者拉取方式给消费者进行消费.
2、路由键 ( RoutingKey)
生产者将消息发送给交换机的时候, 会指定RoutingKey指定路由规则。
3、绑定键 ( BindingKey)
通过绑定键将交换机与队列关联起来, 这样RabbitMQ就知道如何正确地将消息路由到队列。
4、关系
生产者将消息发送给哪个Exchange是需要由RoutingKey决定的.
生产者需要将Exchange与哪个队列绑定时需要由 BindingKey决定的。
当routing key和binding key能对应上的时候就发到相应的队列中。
交换机类型和区别
1、直连交换机: Direct exchange (点对点)
直连交换机的路由算法非常简单: 将消息推送到binding key与该消息的routing key相同的队列。
直连交换机X上绑定了两个队列。第一个队列绑定了绑定键orange, 第二个队列有两个绑定键: black和green。
在这种场景下,一 个消息在布时指定了路由键为orange将会只被路由到队列Q1
路由键为black 和green的消息都将被路由到队列Q2。其他的消息都将被丢失。
同一个绑定键可以绑定到不同的队列上去, 可以增加一个交换机X与队列Q2的绑定键,在这种清况下,直连交换机将会和广播交换机有着相同的行为, 将消息推送到所有匹配的队列。一个路由键为black的消息将会同时被推送到队列Q1和Q2。
它有一个特殊的属性使得它对于简单应用特别有用处 :那就是每个新建队列 (queue) 都会自动绑定到默认交换机上,绑定的 路由键(routing key) 名称与队列名称相同。
当你声明了一个名为“hello”的队列,RabbitMQ会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为“hello”。
当携带着名为“hello”的路由键的信息被发送到默认交换机的时候,此消息会被默认交换机路由至名为“hello”的队列中
类似amq.*的名称的交换机:这些是RabbitMQ默认创建的交换机。
这些队列名称被预留做RabbitMQ内部使用,不能被应用使用,否则抛出403错误
2、主题交换机: Topic exchange (发布订阅)
直连交换机的缺点:
直连交换机的 routing_key方案非常简单 ,如果我们希望一 条消息发送给多个队列 ,那么这个交换机需 要绑定上非常多的 routing_key.
假设每个交换机上都绑定一堆的 routing_key连接到各个队列上。那么消息的管理 就会异常地困难。
主题交换机的特点:
发送到主题交换机的 消息不能有任意的 routing key, 必须是由点号分开的一串单词,这些单词可以是任意的,但通常是与消息相关的一些特征。
如以下是几个有效的routing key:
“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabb 代”, routing key的单词可以 有很多,最大限制是255 bytes。
Topic 交换机的 逻辑与 direct 交换机有点 相似 使用特定路由键发送的消息 将被发送到所有使用匹配绑定键绑定的队列 ,然而 ,绑定键有两个特殊的情况:
*表示匹配任意一个单词
#表示匹配任意—个或多个单词
如:
routing key quick.orange.rabbit-> queue Ql, Q2
routing key lazy.orange.elephant-> queue Ql,Q2
延申:
当一个队列的绑定键是"#",它将会接收所有的消息,而不再考虑所接收消息的路由键。
当一个队列的绑定键没有用到"#"和’*"时,它又像 direct 交换一样工作。
3、扇形交换机: Fanout exchange (广播)
扇形交换机是最基本的交换机类型,它所能做的事情非常简单:广播消息。
扇形交换机会把能接收到的消息全部发送给绑定在自己身上的队列。
因为广播不需要'思考”,所以扇形交换机处理消息的速度也是所有的交换机类型里面最快的。
4、首部交换机: Headers exchange
类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
此交换机有个重要参数:”x-match”
当”x-match”为“any”时,消息头的任意一个值被匹配就可以满足条件
当”x-match”设置为“all”的时候,就需要消息头的所有值都匹配成功
5、Dead Letter Exchange (死信交换机)
RabbitMQ作为一个高级消息中间件,提出了死信交换器的概念。
这种交互器专门处理死了的信息(被拒绝可以重新投递的信息不能算死的)。
消息变成死信一般是以下三种情况:
①、消息被拒绝,并且设置requeue参数为false。
②、消息过期(默认情况下Rabbit中的消息不过期,但是可以设置队列的过期时间和信息的过期的效果)
③、队列达到最大长度(一般当设置了最大队列长度或大小并达到最大值时)
当满足上面三种情况时,消息会变成死信消息,并通过死信交换机投递到相应的队列中。
我们只需要监听相应队列,就可以对死信消息进行最后的处理。
订单超时处理:
生产者生产一条1分钟后超时的订单信息到正常交换机exchange-a中,消息匹配到队列queue-a,但一分钟后仍未消费。
消息会被投递到死信交换机dlx-exchange中,并发送到私信队列中。
死信队列dlx-queue的消费者拿到信息后,根据消息去查询订单的状态,如果仍然是未支付状态,将订单状态更新为超时状态。
交换机的属性
Name:交换机名称
Type:交换机类型,direct,topic,fanout,headers
Durability:是否需要持久化,如果持久性,则RabbitMQ重启后,交换机还存在
Auto Delete:当最后一个绑定到Exchange上的队列删除后,自动删除该Exchange
Internal:当前Exchange是否用于RabbitMQ内部使用,默认为false。
Arguments:扩展参数,用于扩展AMQP协议定制使用
RabbitMQ的工作模式
1.简单模式
简单模式是最简单的消息模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。
2.工作模式
工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、两个消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息。
3.发布/订阅模式
发布/订阅模式是指同时向多个消费者消费消息的模式(类似广播的形式),它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。
4.路由模式
路由模式是可以根据路由键选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过绑定键绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键转发到不同队列,队列绑定的消费者接收并消费消息
(要求路由键和绑定键相同)
常用的交换器主要分为一下三种:
fanout:如果交换器收到消息,将会广播到所有绑定的队列上
direct:如果路由键完全匹配,消息就被投递到相应的队列
topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符
5. 通配符模式
就是绑定键是某种泛泛的符号规则,如果传过来的消息路由键与绑定键匹配,就能传递到对应的消息队列上
特殊匹配符号
*:只能匹配一个单词;
#:可以匹配零个或多个单词。
RabbitMQ工作流程
生产者消息投递过程
生产者连接到Broker 建立一个连接,然后开启一个信道
接着生产者声明一个交换器 ,并设置相关属性,比如交换机类型、是否持久化、是否自动删除、是否内置等
生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除、消息最大过期时间、消息最大长度、消息最大字节数等
生产者通过绑定键将交换器和队列绑定起来
生产者发送消息至Broker ,发送的消息包含消息体和路由规则(可以理解为路由键)、交换器、优先级、是否持久化、过期时间、延时时间等信息的标签
相应的交换器根据接收到的路由键和与队列的绑定键查找相匹配的队列如果找到 ,则将从生产者发送过来的消息存入相应的队列中
如果没有找到 ,则根据生产者配置的属性选择丢弃还是回退给生产者
关闭信道
关闭连接
消费者消费消息过程
消费者连接到Broker ,建立一个连接,开启一个信道
消费者向 RabbitMQ Broker 请求消费相应队列中的消息,在这个过程中可能会设置消费者标签、是否自动确认、是否排他等
等待 RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
消费者确认接收到的消息
RabbitMQ从队列中删除相应己经被确认的消息
关闭信道
关闭连接。
涉及名词解释
在上方的消息流转过程中涉及了以下几个名词
是否持久化
将数据持久化到磁盘中
是否自动删除
当一个队列或交换机的所有消费者都与之断开连接时则这个队列或交换机就会自动删除
是否内置
客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式
是否排他
如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意的是:
排他队列是基于连接可见的,同一个连接的不同信道是可以同时访问同一连接创建的排他队列; "首次"是指如果一个连接己经声明了排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
自动确认
消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 等于 false时, RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)
当 autoAck 等于 true 时, RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息
采用消息确认机制后,只要设置 autoAck 参数为 false ,消费者就有足够的时间处理消息,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题。因为 RabbitMQ 会一直等待持有消息直到消费者显式确认收到消息
说一下消息确认机制和返回机制,有什么区别?
为了保证 RabbitMQ 中消息的可靠性投递,以及消息在发生特定异常时的补偿策略,RabbitMQ诞生了消息确认和返回机制.
消息确认机制(也叫应答机制)
消息确认机制,是保障消息与 RabbitMQ消息之间可靠传输消息一种保障机制
其主要内容就是用来监听RabbitMQ消息队列收到消息之后返回给生产端的ack确认消息
消息确认机制描述了一种消息是否已经被发送到 RabbitMQ消息队列中以及 RabbitMQ消息队列是否以及接收到生产端发送的消息。
消息确认机制的作用
监听生产者的消息是否已经发送到了 RabbitMQ消息队列中;
如果消息没有被发送到 RabbitMQ消息队列中,则消息确认机制不会给生产端返回任何确认应答,也就是没有发送成功
相反,如果消息被成功发送到了 RabbitMQ消息队列中,则消息确认机制会给生产端返回一个确认应答,以通知生产者,消息已经发送到了 RabbitMQ消息队列
消息返回机制
描述不可达的消息与生产者之间的一种保障策略
其主要是用来监听,RabbitMQ消息队列中是否存在不可达的消息,并根据监听结果返回给生产端的一种监听机制
消息返回机制描述了一种 RabbitMQ消息队列中的不可达消息与生产端的关系
什么是不可达的消息
消息在被成功发送到RabbitMQ消息队列中之后,如果消息在经过当前配置的 exchangeName 或 routingKey 没有找到指定的交换机,或没有匹配到对应的消息队列,
那么这个消息就被称为不可达的消息,如果此时配置了消息返回机制,那么此时RabbitMQ消息队列会返回给生产者一个信号,信号中包括消息不可达的原因,以及消息本身的内容。
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("return relyCode: " + replyCode); // 状态码
System.out.println("return replyText: " + replyText); // 结果信息
System.out.println("return exchange: " + exchange);
System.out.println("return routingKey: " + routingKey);
System.out.println("return properties: " + properties);
System.out.println("return body: " + new String(body));
}
});
Rabbitmq的持久化机制
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。所以就要对消息进行持久化处理。
rabbitmq的持久化分为队列持久化、消息持久化和交换器持久化。
- 队列的持久化是在定义队列时的durable参数来决定的,当durable为true时,才代表队列会持久化。
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//第二个餐胡设置为true,代表队列持久化
channel.queueDeclare("queue.persistent.name", true, false, false, null);
- 如果要在重启后保持消息的持久化必须设置消息是持久化的标识。
//通过传入MessageProperties.PERSISTENT_PLAIN就可以实现消息持久化
channel.basicPublish("exchange.persistent", "persistent", MessageProperties.PERSISTENT_TEXT_PLAIN, "persistent_test_message".getBytes());
- 上面阐述了队列的持久化和消息的持久化,如果不设置exchange的持久化对消息的可靠性来说没有什么影响,但是同样如果exchange不设置持久化,那么当broker服务重启之后,exchange将不复存在,那么既而发送方rabbitmq producer就无法正常发送消息。这里建议,同样设置exchange的持久化。exchange的持久化设置也特别简单.
所以一般三个持久化设置都要进行.
可以对所有消息都持久化吗?
不可以
持久化的操作是将数据写入磁盘中,效率上肯定比写入内存中要慢很多倍.而我们一般用mq会处理很多业务消息,若是所有消息都持久化,压力无疑是巨大的.所以持久化策略需要综合考虑,以及可能遇到的问题和解决方案,或者我们可以让一些必要数据持久化.
Rabbitmq的应答机制
一、消息应答机制
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉,由于RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息,以及后续发送给该消费者的消息也无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制。
消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除掉了。
二、自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
三、手动消息应答的方法
//用于肯定确认,RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
channel.basicAck();
//用于否定确认
channel.basicNack();
//用于否定确认,不处理该消息了直接拒绝,可以将其丢弃了
channel.basicReject();
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
Rabbitmq事务机制
1、概述
在使用RabbitMQ的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的消息丢失,除此之外我们还会遇到一个问题,当消息的发布者在将消息发送出去之后,消息到底有没有正确到达broker代理服务器呢?如果不进行特殊配置的话,默认情况下发布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达broker的,如果在消息到达broker之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,你怎么进行持久化,那么这个问题该怎么解决呢?
RabbitMQ为我们提供了两种方式:
通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案;
通过将channel设置成confirm模式来实现;
2、事务机制
这里首先探讨下RabbitMQ事务机制。
RabbitMQ中与事务机制有关的方法有三个:txSelect(), txCommit()以及txRollback(), txSelect用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务,在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。
public class P1 {
private static final String QUEUE_NAME = "test_tx";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,true,null);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
Date date = new Date(System.currentTimeMillis());
String message = simpleDateFormat.format(date);
try {
channel.txSelect();//开始事务
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
channel.txCommit();//提交事务
}catch (Exception e){
channel.txRollback();//回滚事务
System.out.println("send message txRollback");
}
channel.close();
connection.close();
}
}
消息分发策略有哪些?
1、轮询分发
RabbitMQ 分发消息默认采用的轮询分发,但是在某种场景下这种策略并不是很好,当有两个消费者在处理任务时,其中有个消费者 处理任务的速度非常快,而另外一个消费者处理速度却很慢,这个时候我们还是采用轮询分发就会导致这处理速度快的这个消费者很大一部分时间处于空闲状态。我们可以通过修改消息分发的默认机制,来达到优化目的
2、不公平分发
通过设置参数 channel.basicQos(1);实现不公平分发策略使得能者多劳
通过RabbitMq的Web管理页面,可以看到Channels的Prefetch count属性显示为1则表示不公平分发成功
上面介绍了basicQos,如果我们将qos的值设为1,那么你想一想会出现什么情况呢?信道中只允许传输一条消息,那么当这条消息处理完后,队列会立马发送下一条消息,所以这个时候快的不断处理,慢的等待当前处理完再处理下一条。这样就实现了能者多劳。
3、预值分发
当消息被消费者接收后,但是没有确认,此时这里就存在一个未确认的消息缓冲区,用于存储非被确认的消息,该缓存区的大小是没有限制的。
预取值: 定义通道上允许的未确认消息的最大数量。一旦未确认消息数量达到配置的最大数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认;例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取值计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack;例如, tag=6 这个消息刚刚被确认 ACK,此时RabbitMQ 将会感知这个情况到并再发送一条消息。
如果消费者消费了大量的消息但是没有确认的话,就会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量。
其实预取值设置就是一个非公平分发策略,我们通过设置qos来设置消息缓冲区允许未确认消息的最大数量,当我们设置为1时,是最保守的,吞吐量也是最低的,而100到300这个范围通常是最佳的.
如何保证消息的顺序消费
消息在投入到queue的时候是有顺序,如果只是单个消费者来处理对应的单个queue,是不会出现消息错乱的问题。但是在消费的时候有可能多个消费者消费同一个queue,由于各个消费者处理消息的时间不同,导致消息未能按照预期的顺序处理。其实根本的问题就是如何保证消息按照预期的顺序处理完成。
出现消费顺序错乱的情况
- 为了提高处理效率,一个queue存在多个consumer
2. 一个queue只存在一个consumer,但是为了提高处理效率,consumer中使用了多线程进行处理
保证消息顺序性的方法
将原来的一个queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据) 发送到同一个queue当中
一个queue就一个consumer,在consumer中维护多个内存队列,根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。
总结:
核心思路就是根据业务数据关键值划分成多个消息集合,而且每个消息集合中的消息数据都是有序的,每个消息集合有自己独立的一个consumer。多个消息集合的存在保证了消息消费的效率,每个有序的消息集合对应单个的consumer也保证了消息消费时的有序性。
消息队列如何保证消息的可靠性传输
消息的可靠性传输分为两个问题,一个是保证消息不被重复消费,另一个是保证消息不丢失
保证消息不重复被消费,就是保证消息的幂等性问题,消息的幂等性是指一个操作执行任意多次所产生的影响均与一次执行的影响相同,在mq里,也就是消息只能被消费一次,不能被重复消费.
消息丢失如何处理:
发送方丢失,可能发送方在发送消息的过程中,出现网络问题等导致mq接收不到消息,导致了消息丢失.
要解决这个问题,首先可以采用事务机制,在发送消息的时候实现事务机制,若是出现发送失败的情况,可以进行回滚,而让消息重新被发送.但是开启了事务,发送方就必须同步等待事务执行完毕或者回滚,导致消息一多,性能会下降.
但是,还有一个更好的办法:可以采用确认机制,发送方在发送消息的时候必须要保证要收到一个确认消息,如果没有收到或者收到失败的确认消息,就说明消息发送失败,要重新进行发送,确认机制是可以采用异步进行的,这样就极大地保证了在保留效率的基础上又能保证消息的不丢失问题.
第二个丢失问题可能是在mq方发生的,如果mq没有进行持久化,出现了宕机关机等情况,消息就会丢失,解决办法无非就是将消息进行持久化,这样在出现问题的时候可以及时对消息进行恢复.
第三个丢失问题可能在消费方发生,这和发送方丢失问题类似,解决这个问题也是采用确认机制,这样一来就可以实现效率上的保证和消息不丢失的保证
.
消息重复如何处理
但是解决了这些问题,就会产生下面的幂等性问题:
我们都知道mq是可以进行重发的,且只有在它认为失败的情况会进行重发.什么时候mq会认为它发送给消费者的消息是失败的呢?也就是超出了它等待消费者响应的时间,这是一个超时时间,若是过了这个时间消费者仍然没有响应,说明mq发送失败,就会进行重试,而其实这个时候消费者可能是没有失败的,它只是因为某个原因导致消费超出了mq的等待时间而已,这个时候mq再发送一次消息,消费者就会重复消费.
实现幂等性消费:
① 通过数据库:比如处理订单时,记录订单ID,在消费前,去数据库中进行查询该记录是否存在,如果存在则直接返回。
② 使用全局唯一ID,再配合第三组主键做消费记录,比如使用 redis 的 set 结构,生产者发送消息时给消息分配一个全局ID,在每次消费者开始消费前,先去redis中查询有没有消费记录,如果消费过则不进行处理,如果没消费过,则进行处理,消费完之后,就将这个ID以k-v的形式存入redis中(过期时间根据具体情况设置)。
如何处理消息堆积情况
场景题:几千万条数据在MQ里积压了七八个小时。
出现该问题的原因
消息堆积往往是生产者的生产速度与消费者的消费速度不匹配导致的。有可能就是消费者消费能力弱,渐渐地消息就积压了,也有可能是因为消息消费失败反复复重试造成的,也有可能是消费端出了问题,导致不消费了或者消费极其慢。比如,消费端每次消费之后要写mysql,结果mysql挂了,消费端hang住了不动了,或者消费者本地依赖的一个东西挂了,导致消费者挂了。
所以如果是 bug 则处理 bug;如果是因为本身消费能力较弱,则优化消费逻辑,比如优化前是一条一条消息消费处理的,那么就可以批量处理进行优化。
临时扩容,快速处理积压的消息:
(1)先修复 consumer 的问题,确保其恢复消费速度,然后将现有的 consumer 都停掉;
(2)临时创建原先 N 倍数量的 queue ,然后写一个临时分发数据的消费者程序,将该程序部署上去消费队列中积压的数据,消费之后不做任何耗时处理,直接均匀轮询写入临时建立好的 N 倍数量的 queue 中;
(3)接着,临时征用 N 倍的机器来部署 consumer,每个 consumer 消费一个临时 queue 的数据
(4)等快速消费完积压数据之后,恢复原先部署架构 ,重新用原先的 consumer 机器消费消息。
这种做法相当于临时将 queue 资源和 consumer 资源扩大 N 倍,以正常 N 倍速度消费。
MQ长时间未处理导致MQ写满的情况如何处理:
如果消息积压在MQ里,并且长时间都没处理掉,导致MQ都快写满了,这种情况肯定是临时扩容方案执行太慢,这种时候只好采用 “丢弃+批量重导” 的方式来解决了。首先,临时写个程序,连接到mq里面消费数据,消费一个丢弃一个,快速消费掉积压的消息,降低MQ的压力,然后在流量低峰期时去手动查询重导丢失的这部分数据
短时间内无法扩容或者扩容无法完全解决问题
可以尝试降低一些非核心业务的消息处理,其次可以通过监控排查,优化消费者端的业务代码或者查看是否存在一些消息被重复消息的情况.
如何保证消息队列的高可用?
RabbitMQ 是基于主从(非分布式)做高可用性的。
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式
单机模式:
一般没人生产用单机模式
普通集群模式:
普通集群模式用于提高系统的吞吐量,通过添加节点来线性扩展消息队列的吞吐量。
也就是在多台机器上启动多个 RabbitMQ 实例,而队列 queue 的消息只会存放在其中一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。消费的时候,如果连接到了另外的实例,那么该实例就会从数据实际所在的实例上的queue拉取消息过来,就是说让集群中多个节点来服务某个 queue 的读写操作
但普通集群模式的缺点在于:无高可用性,queue所在的节点宕机了,其他实例就无法从那个实例拉取数据;RabbitMQ 内部也会产生大量的数据传输。
镜像队列集群模式:
镜像队列集群是RabbitMQ 真正的高可用模式,集群中一般会包含一个主节点master和若干个从节点slave,如果master由于某种原因失效,那么按照slave加入的时间排序,"资历最老"的slave会被提升为新的master。
镜像队列下,所有的消息只会向master发送,再由master将命令的执行结果广播给slave,所以master与slave节点的状态是相同的。比如,每次写消息到 queue 时,master会自动将消息同步到各个slave实例的queue;如果消费者与slave建立连接并进行订阅消费,其实质上也是从master上获取消息,只不过看似是从slave上消费而已,比如消费者与slave建立了TCP连接并执行Basic.Get的操作,那么也是由slave将Basic.Get请求发往master,再由master准备好数据返回给slave,最后由slave投递给消费者。
从上面可以看出,队列的元数据和消息会存在于多个实例上,也就是说每个 RabbitMQ 节点都有这个 queue 的完整镜像,任何一个机器宕机了,其它机器节点还包含了这个 queue 的完整数据,其他消费者都可以到其它节点上去消费数据。
(1)缺点:
① 性能开销大,消息需要同步到所有机器上,导致网络带宽压力和消耗很重
② 非分布式,没有扩展性,如果 queue 的数据量大到这个机器上的容量无法容纳了,此时该方案就会出现问题了
(2)如何开启镜像集群模式呢?
在RabbitMQ 的管理控制台Admin页面下,新增一个镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
谈谈你对死信队列的理解
概念
当queue消息队列中的消息由于一些原因没办法被消费,如果这些消息一直没办法被处理,就会从这个正常的消息队列转移到死信消息队列中。
应用场景
当用户下单之后,如果一直不支付,那么用户下单的这条消息就会被存放到死信队列中去。
死信的来源
1.当queue消息队列满的时候,再进来的消息就会被放到死信队列中去。
2.在消息应答的时候,如果消费者一直没有告诉RabbitMQ有没有成功处理消息,那么RabbitMQ消息队列就不清楚自己到底要不要删除这条消息,这个时候消息队列中的消息一直没办法处理,这样这条消息也会被放到死信队列中去。
3.消息TTL过期,什么意思?TTL的全拼是Time To Live意思是指存活时间,就是消息队列中存放的消息一般都是有一定时间的,超过了这个时间,这条消息就会被放到死信队列中去。
如何去实现死信队列?
生产者:
发送消息.
普通消费者:
1)在用Map声明普通队列参数的时候,可以先设置死信队列的路由key和交换机,让普通该队列处理不来的消息转到对应的死信队列上.
2)然后声明普通队列,普通交换机,死信队列,死信交换机.
3)之后消费普通队列消息文章来源:https://www.toymoban.com/news/detail-402025.html
public class Consumer01 {
//普通交换机的名称
public static final String NORMAL_EXCHANGE="normal_exchange";
//死信交换机的名称
public static final String DEAD_EXCHANGE="dead_exchange";
//普通队列的名称
public static final String NORMAL_QUEUE="normal_queue";
//死信队列的名称
public static final String DEAD_QUEUE="dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtil.getChannel();
//声明死信和普通交换机类型为direct类型
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明普通队列
Map<String,Object> arguments=new HashMap<>();
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
arguments.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定普通的交换机与普通的队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信的交换机与死信的队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//如果能成功接收到消息会调用的回调函数
DeliverCallback deliverCallback=(consumerTag, message)->{
System.out.println("Consumer01接收者接收到的消息:"+new String(message.getBody()));
};
//如果取消从消息队列中获取消息时会调用的回调函数
CancelCallback cancelCallback= consumerTag->{
System.out.println("消息消费被中断");
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
}
}
死信消费者:
1)消费死信队列即可,消费过程和普通队列一样.文章来源地址https://www.toymoban.com/news/detail-402025.html
到了这里,关于RabbitMQ面试题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!