Java面试题2.0--RabbitMQ

 

欢迎关注《Java面试题2.0》合集发布页,持续更新中!

 
 
什么是RabbitMQ?它的特性是什么
 
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
 
1、可靠性(Reliability)
2、灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
3、消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
4、高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
6、多语言客户端(Many Clients)
RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
 
队列、路由概念
Quque(队列)是RabbitMQ的内部对象,用于存储信息,
生产者将消息发送到Exchange(交换器,下图中X),Exchange将消息路由到一个或者多个Quque中或者丢弃
routing key(路由key)
生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个路由规则,而这个routing key需要与 Exchange Type 与binding key 联合使用才能最终生效。
 
 
RabbitMQ 概念里的 channel、exchange 和 queue 是什么?
queue 具有自己的 erlang 进程;
exchange 内部实现为保存 binding 关系的查找表;
channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue 。
 
 
如何确保消息正确地发送至 RabbitMQ?
RabbitMQ 使用发送方确认模式,确保消息正确地发送到 RabbitMQ。
 
发送方确认模式:将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID 。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(not acknowledged,未确认)消息。
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
 
如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
 
消息的确认模式
 
模式1:自动确认
只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。
 
模式2:手动确认
消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。
 
消息重复
 
造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?
 
消费端处理消息的业务逻辑保持幂等性。
幂等(idempotent、idempotence)是一个数学与计算机学概念。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
 
保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。
 
第 1 条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。第 2 条原理就是利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。
 
第 1 条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。第 2 条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是 RabbitMQ 不解决消息重复的问题的原因。
 
RabbitMQ 不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。
 
RabbitMQ 消息丢失场景有哪些
 
生产者生产消息到RabbitMQ Server 消息丢失场景
1) 外界环境问题导致:发生网络丢包、网络故障等造成RabbitMQ Server端收不到消息,因为生产环境的网络是很复杂的,网络抖动,丢包现象很常见,下面会讲到针对这个问题是如何解决的。
2) 代码层面,配置层面,考虑不全导致消息丢失
 
RabbitMQ Server中存储的消息丢失或可靠性不足
1)消息未完全持久化,当机器重启后,消息会全部丢失,甚至Queue也不见了
2)单节点模式问题,如果某个节点挂了,消息就不能用了,业务可能瘫痪,只能等待
3)普通集群模式:某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(建立在消息持久化)
 
RabbitMQ Server到消费者消息丢失
消费端接收到相关消息之后,消费端还没来得及处理消息,消费端机器就宕机了,此时消息如果处理不当会有丢失风险
 
如何避免消息丢失?
 
生产者生产消息到RabbitMQ Server可靠性保证
1 AMQP协议提供的一个事务机制,但是,很少有人这么干,因为这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低。
 
2、发送方确认机制(publisher confirm)
所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一deliveryTag和multiple参数),这就使得生产者知晓消息已经正确到达了目的地了。
 
消息被正确的发往至RabbitMQ的交换器,如果此交换器没有匹配的队列的话,那么消息也将会丢失,怎么办?
1. 使用mandatory 设置true
当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue, 那么会调用basic.return方法将消息返回给生产者。
当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。
 
2. 利用备份交换机(alternate-exchange):实现没有路由到队列的消息
通过webui管理后台设置,当你新建一个exchange业务的时候,可以给它设置Arguments,这个参数就是 alternate-exchange,其实alternate-exchange就是一个普通的exchange,类型最好是fanout 方便管理。当你发送消息到你自己的exchange时候,对应key没有路由到queue,就会自动转移到alternate-exchange对应的queue,起码消息不会丢失。
 
RabbitMQ Server到消费者消息如何不丢?
采用手动ack机制来解决这个问题,消费端处理完逻辑之后再通知MQ Server,这样消费者没处理完消息不会发送ack,如果在消费者拿到消息,没来得及处理的情况下自己挂了,此时MQ集群会自动感知到,它就会自觉的重发消息给其他的消费者服务实例。
 
消息的高可用问题,单节点问题,普通节点的问题都会影响消息的临时不可用应该怎么处理?
使用镜像模式,镜像模式至少采用3节点,2个磁盘节点和1个内存节点来保证。采用镜像模式,要根据具体的业务规则定制话处理,没那么重要的业务,消息丢了也没关系的场景,又要求必须高的性能的时候,镜像也可以不用设置。
 
如何保证数据的顺序性
 
如果存在多个消费者,那么就让每个消费者对应一个queue,然后把要发送 的数据全都放到一个queue,这样就能保证所有的数据只到达一个消费者从而保证每个数据到达数据库都是顺序的。rabbitmq:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
 
消息发送的整个流程
消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入 “黑洞”。
 
大量消息在 mq 里积压了几个小时了还没解决
 
几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。
 
一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。
 
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
 
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
 
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
 
mq 中的消息堆积的时候,过期失效了
 
假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。
 
这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。
 
假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
 
消息堆积的时候,mq 都快写满了怎么办
 
如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
 
Work模式
 
一个生产者、2个消费者。一个消息只能被一个消费者获取。没有交换机,消息直接进入队列。
 
存在的问题
 
轮询分发 :使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。
 
公平分发 :虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。
 
为了解决这个问题,我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送。
还有一点需要注意,使用公平分发,必须关闭自动应答,改为手动应答。
 
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, false, consumer);
 
 
常用的交换器有哪些
常用的交换器主要分为一下三种:
direct:如果路由键完全匹配,消息就被投递到相应的队列。
fanout:如果交换器收到消息,将会广播到所有绑定的队列上。
topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由键分为了几部分,“#” 匹配所有规则等。特别注意:发往 topic 交换器的消息不能随意的设置选择键(routing_key),必须是由 "." 隔开的一系列的标识符组成。
 
 
RabbitMQ中实现RPC的机制是:
 
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。
 
客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
服务器端收到消息并处理
服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性
客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理
 
高可用集群
 
RabbitMQ 是用 erlang 开发的,集群非常方便,因为 erlang 天生就是一门分布式语言,但其本身并不支持负载均衡。Rabbit 模式大概分为以下三种:单一模式、普通模式和镜像模式。
 
单一模式:
 
最简单的情况,非集群模式。
 
普通模式:默认的集群模式。
 
  对于 Queue 来说,消息实体只存在于其中一个节点,A、B 两个节点仅有相同的元数据,即队列结构。当消息进入 A 节点的 Queue 中后,consumer 从 B 节点拉取时,RabbitMQ 会临时在 A、B 间进行消息传输,把 A 中的消息实体取出并经过 B 发送给 consumer. 所以 consumer 应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理 Queue。否则无论 consumer 连 A 或 B,出口总在 A,会产生瓶颈。该模式存在一个问题就是当 A 节点故障后,B 节点无法取到 A 节点中还未消费的消息实体。如果做了消息持久化,那么得等 A 节点恢复,然后才可被消费;如果没有持久化的话,然后就没有然后了……
 
镜像模式:
 
把需要的队列做成镜像队列,存在于多个节点,属于 RabbitMQ 的 HA 方案。
该模式解决了上述问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在 consumer 取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉,所,在对可靠性要求较高的场合中适用。
 
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
 
那么如何开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
 
这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就没有扩展性可言了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并没有办法线性扩展你的 queue。
 
 
RabbitMQ 中的 Broker 是指什么?Cluster 又是指什么?
Broker ,是指一个或多个 erlang node 的逻辑分组,且 node 上运行着 RabbitMQ 应用程序。
Cluster ,是在 Broker 的基础之上,增加了 node 之间共享元数据的约束。
 
 
什么是元数据?元数据分为哪些类型?包括哪些内容?
在非 Cluster 模式下,元数据主要分为:
Queue 元数据(queue 名字和属性等)
Exchange 元数据(exchange 名字、类型和属性等)
Binding 元数据(存放路由关系的查找表)
Vhost 元数据(vhost 范围内针对前三者的名字空间约束和安全属性设置)。
 
 
 
 
 
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值