RabbitMQ常见⾯试题
什么是RabbitMQ?
RabbitMQ是⼀款开源的,Erlang编写的,基于AMQP协议的消息中间件,核⼼思想是⽣产者不会将消息直接发送给队列,消息在发给客户端时会先发给交换机,然后再由交换机发送给对应的队列。
RabbitMQ有什么优缺点?
优点:
解耦
系统A在代码中直接调⽤系统B和系统C的代码,如果将来D系统接⼊,系统A还需要修改代码,过于⿇烦!
异步
将消息写⼊消息队列,⾮必要的业务逻辑以异步的⽅式运⾏,加快响应速度
削峰
并发量⼤的时候,所有的请求直接怼到数据库,造成数据库连接异常
缺点:
系统的可⽤性降低
系统引⼊的外部依赖越多,系统越容易挂掉,本来只是A系统调⽤BCD三个系统接⼝就好,ABCD四个系统不报错整个系统会正常运⾏。引⼊了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。
系统的复杂性提⾼
引⼊了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序?
⼀致性问题
A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产⽣数据不⼀致的问题。
RabbitMQ组件介绍
Broker:简单来说就是消息队列服务器实体。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息队列载体,每个消息都会被投⼊到⼀个或多个队列。
Binding:绑定,它的作⽤就是把exchange和queue按照路由规则绑定起来。
Routing Key:路由关键字,exchange根据这个关键字进⾏消息投递。
vhost:虚拟主机,⼀个broker⾥可以开设多个vhost,⽤作不同⽤户的权限分离。
producer:消息⽣产者,就是投递消息的程序。
consumer:消息消费者,就是接受消息的程序。
channel:消息通道,在客户端的每个连接⾥,可建⽴多个channel,每个channel代表⼀个会话任务。
交换机的⼏种类型?
直连交换机(Direct Exchange):根据消息携带的路由键将消息投递给对应队列。
扇型交换机(Fanout Exchange):这个交换机没有路由键概念,就算你绑了路由键也是⽆视的。 这个交换机在接收到消息后,会直接转发到绑定到它上⾯的所有队列。
主题交换机(Topic Exchange):这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。*(星号) ⽤来表⽰⼀个单词 (必须出现的),#(井号) ⽤来表⽰任意数量(零个或多个)单词
⽣产者发送消息过程?
1.Producer先连接到Broker,建⽴连接Connection,开启⼀个信道(Channel)。
2.Producer声明⼀个交换器并设置好相关属性。
3.Producer声明⼀个队列并设置好相关属性。
4.Producer通过路由键将交换器和队列绑定起来。
5.Producer发送消息到Broker,其中包含路由键、交换器等信息。
6.相应的交换器根据接收到的路由键查匹配的队列。
7.如果到,将消息存⼊对应的队列,如果没有到,会根据⽣产者的配置丢弃或者退回给⽣产者。
8.关闭信道。
9.管理连接。
消费者接收消息过程?
1.Producer先连接到Broker,建⽴连接Connection,开启⼀个信道(Channel)。
2.向Broker请求消费响应的队列中消息,可能会设置响应的回调函数。
3.等待Broker回应并投递相应队列中的消息,接收消息。
4.消费者确认收到的消息,ack。
5.RabbitMq从队列中删除已经确定的消息。
6.关闭信道。
7.关闭连接。
什么是死信队列?
DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在⼀个队列中变成死信 (dead message) 之后,它能被重新被发送到另⼀个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
导致的死信的⼏种原因?
消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
消息TTL过期。
队列满了,⽆法再添加。
什么是延时队列?
延时队列,⾸先,它是⼀种队列,队列意味着内部的元素是有序的,元素出队和⼊队是有⽅向性的,元素从⼀端进⼊,从另⼀端取出。
延时队列使⽤场景?
有程序正在修改镜像劫持
1、订单在⼗分钟之内未⽀付则⾃动取消。
2、账单在⼀周内未⽀付,则⾃动结算。
3、⽤户注册成功后,如果三天内没有登陆则进⾏短信提醒。
如何确保消息正确地发送⾄RabbitMQ?
RabbitMQ使⽤发送⽅确认模式,确保消息正确地发送到RabbitMQ。发送⽅确认模式:将信道设置成confirm模式(发送⽅确认模式),则所有在信道上发布的消息都会被指派⼀个唯⼀的ID。⼀旦消息被投递到⽬的队列后,或者消息被写⼊磁盘后(可持久化的消息),信道会发送⼀个确认给⽣产者(包含消息唯⼀ID)。如果RabbitMQ发⽣内部错误从⽽导致消息丢失,会发送⼀条nack(not acknowledged,未确认)消息。发送⽅确认模式是异步的,⽣产者应⽤程序在等待确认的同时,可以继续发送消息。当确认消息到达⽣产者应⽤程序,⽣产者应⽤程序的回调⽅法就会被触发来处理确认消息。
如何确保消息接收⽅消费了消息?
接收⽅消息确认机制:消费者接收每⼀条消息后都必须进⾏确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消
息,RabbitMQ才能安全地把消息从队列中删除。这⾥并没有⽤到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer⾜够长的时间来处理消息。
⼏种特殊情况:
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下⼀个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
如何保证RabbitMQ消息队列的⾼可⽤?
RabbitMQ 有三种模式:单机模式,普通集模式,镜像集模式。
单机模式:就是demo级别的,⼀般就是你本地启动了玩玩⼉的,没⼈⽣产⽤单机模式
普通集模式:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动⼀个。
镜像集模式:这种模式,才是所谓的RabbitMQ的⾼可⽤模式,跟普通集模式不⼀样的是,你创建的queue,⽆论元数据(元数据指RabbitMQ的配置数据)还是queue⾥的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会⾃动把消息到多个实例的queue⾥进⾏消息同步。
如何保证RabbitMQ不被重复消费?
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送⼀个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;
但是因为⽹络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道⾃⼰已经消费过该消息了,再次将消息分发给其他的消费者。
针对以上问题,⼀个解决思路是:保证消息的唯⼀性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;
⽐如:在写⼊消息队列的数据做唯⼀标⽰,消费消息时,根据唯⼀标识判断是否消费过;
如何保证消息的⼀致性?
消息不可靠的情况可能是消息丢失,劫持等原因;
丢失⼜分为:⽣产者丢失消息、消息列表丢失消息、消费者丢失消息;
⽣产者丢失消息:从⽣产者弄丢数据这个⾓度来看,RabbitMQ提供transaction和confirm模式来确保⽣产者不丢消息;
transaction机制就是说:发送消息前,开启事务(Select()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(Rollback()),如果发送成功则提交事务(Commit())。然⽽,这种⽅式有个缺点:吞吐量下降;
confirm模式⽤的居多:⼀旦channel进⼊confirm模式,所有在该信道上发布的消息都将会被指派⼀个唯⼀的ID(从1开始),⼀旦消息被投递到所有匹配的队列之后;
rabbitMQ就会发送⼀个ACK给⽣产者(包含消息的唯⼀ID),这就使得⽣产者知道消息已经正确到达⽬的队列了;
如果rabbitMQ没能处理该消息,则会发送⼀个Nack消息给你,你可以进⾏重试操作。
消息队列丢数据:消息持久化。
处理消息队列丢数据的情况,⼀般是开启持久化磁盘的配置。
这个持久化配置可以和confirm机制配合使⽤,你可以在消息持久化磁盘后,再给⽣产者发送⼀个Ack信号。
这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么⽣产者收不到Ack信号,⽣产者会⾃动重发。
那么如何持久化呢?
这⾥顺便说⼀下吧,其实也很容易,就下⾯两步
将queue的持久化标识durable设置为true,则代表是⼀个持久的队列
发送消息的时候将deliveryMode=2
这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据
消费者丢失消息:消费者丢数据⼀般是因为采⽤了⾃动确认消息模式,改为⼿动确认消息即可!
消费者在收到消息之后,处理消息之前,会⾃动回复RabbitMQ已收到消息;
如果这时处理消息失败,就会丢失该消息;
解决⽅案:处理消息成功后,⼿动回复确认消息。
如何保证消息的顺序性?
如果存在多个消费者,那么就让每个消费者对应⼀个queue,然后把要发送 的数据全都放到⼀个queue,这样就能保证所有的数据只到达⼀个消费者从⽽保证每个数据到达数据库都是顺序的。rabbitmq:拆分多个queue,每个queue⼀个consumer,就是多⼀些queue⽽已,确实是⿇烦点;或者就⼀个queue但是对应⼀个consumer,然后这个consumer内部⽤内存队列做排队,然后分发给底层不同的worker来处理。