文章目录
1. 消息基于什么传输?
由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。
- RabbitMQ使用
信道
的方式来传输数据。 - 信道是建立在真实的TCP连接内的虚拟连接,且
每条TCP连接上的信道数量没有限制
。
2. 消息怎么路由?
从概念上来说,消息路由必须有三部分:交换器
、路由
、绑定
。
生产者把消息发布到交换器上;绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。
- 消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
- 通过队列路由键,可以把队列绑定到交换器上。
- 消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。
- 如果能够匹配到队列,则消息会投递到相应队列中;
- 如果不能匹配到任何队列,消息将进入 “黑洞”。
常用的交换器主要分为一下三种:
direct(路由模型)
:如果路由键完全匹配,消息就被投递到相应的队列fanout(广播模型)
:如果交换器收到消息,将会广播到所有绑定的队列上topic(订阅模型)
:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由键分为了几部分,“#” 匹配所有规则等。- 特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是由"."隔开的一系列的标识符组成。
3. 如何确保消息正确地发送至RabbitMQ?
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。
发送方确认模式
:
- 将
信道
设置成confirm模式
(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID
。 - 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。
- 如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。
发送方确认模式是异步的
,生产者应用程序在等待确认的同时,可以继续发送消息。- 当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
4. 如何确保消息接收方消费了消息?
接收方消息确认机制
:
- 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。
-只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除
。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息
。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。
下面罗列几种特殊情况:
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
5. 如何避免消息重复投递或重复消费?
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id
,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;
在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
这个问题针对业务场景来答分以下几点:
-
1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
-
2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
-
3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
6. 如何解决丢数据的问题?
6.1 生产者丢数据
生产者的消息没有投递到MQ中怎么办?
从生产者弄丢数据这个角度来看,RabbitMQ提供transaction
和confirm
模式来确保生产者不丢消息。
-
transaction机制就是说,发送消息前,开启事物(
channel.txSelect()
),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()
),如果发送成功则提交事物(channel.txCommit()
)。缺点就是吞吐量下降了
。 -
因此,生产上用
confirm模式
的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
6.2 消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
这个持久化配置可以和confirm
机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。
这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步
-
①、将queue的持久化标识
durable设置为true
,则代表是一个持久的队列 -
②、发送消息的时候将
deliveryMode=2
这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。
在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)
6.3 消费者丢数据
启用手动确认模式可以解决这个问题
-
①自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。
-
②手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。
-
③不确认模式,acknowledge=“none” 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。
7. 死信队列和延迟队列的使用
7.1 死信队列
:
消息被拒绝
(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false消息过期了
队列达到最大的长度
在 rabbitmq 中存在2种方可设置消息的过期时间
- 第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间
- 第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。
如果同时使用这2种方法,那么以过期时间小的那个数值为准。
当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信
消息。
-
队列设置:在队列申明的时候使用
x-message-ttl
参数,单位为 毫秒 -
单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒
7.2 延时队列
:
在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。
消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
8. "dead letter"queue(死信队列) 的用途?
当消息被 RabbitMQ server 投递到 consumer 后,但 consumer 却通过 Basic.Reject 进行了拒绝时(同时设置 requeue=false
),那么该消息会被放入“dead letter”queue 中。该 queue 可用于排查 message 被 reject 或 undeliver 的原因
。
9. message 被可靠持久化的条件
- binding 关系可以表示为 exchange – binding – queue 。
若要求投递的 message 能够不丢失
- 要求 message 本身设置 persistent 属性
- 要求 exchange 和 queue 都设置 durable 属性。
其实这问题可以这么想,若 exchange 或 queue 未设置 durable 属性,则在其 crash 之后就会无法恢复,那么即使 message 设置了 persistent 属性,仍然存在 message 虽然能恢复但却无处容身的问题;同理,若 message 本身未设置 persistent 属性,则 message 的持久化更无从谈起。
10. RabbitMQ的高可用性
rabbitmq有三种模式:单机模式
,普通集群模式
,镜像集群模式
-
普通集群模式
意思就是在多台机器上启动多个rabbitmq实例,每个机器启动一个。
但是你创建的queue,只会放在一个rabbtimq实例上,但是每个实例都同步queue的元数据。 -
镜像集群模式
这种模式,才是所谓的rabbitmq的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。
开启这个镜像集群模式就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候可以要求数据同步到所有节点的,也可以要求就同步到指定数量的节点,然后你再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上
好处: 任何一个机器宕机了,没事儿,别的机器都可以用
坏处:- 性能开销也太大了吧,消息同步所有机器,导致网络带宽压力和消耗很重;
- 没有扩展性可言,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法拓展;
11. 异步刷盘与同步刷盘的区别
同步刷盘和异步刷盘的区别如下:
- 同步刷盘:
当数据写如到内存中之后立刻刷盘(同步),在保证刷盘成功的前提下响应client。 - 异步刷盘:
数据写入内存后,直接响应client。异步将内存中的数据持久化到磁盘上。
同步刷盘和异步输盘的优劣:
- 同步刷盘保证了数据的可靠性,保证数据不会丢失。
- 同步刷盘效率较低,因为client获取响应需要等待刷盘时间,为了提升效率,通常采用批量输盘的方式,每次刷盘将会flush内存中的所有数据。(若底层的存储为mmap,则每次刷盘将刷新所有的dirty页)
- 异步刷盘不能保证数据的可靠性.
- 异步刷盘可以提高系统的吞吐量.
常见的异步刷盘方式有两种,分别是定时刷盘和触发式刷盘。定时刷盘可设置为如每1s刷新一次内存.触发刷盘为当内存中数据到达一定的值,会触发异步刷盘程序进行刷盘。
RocketMQ存在同步刷盘和异步刷盘相关配置,但是rabbitmq是内存换页
12. RabbitMQ 概念里的 channel、exchange 和 queue
- queue 具有自己的 erlang 进程;
- exchange 内部实现为保存 binding 关系的查找表;
- channel 是实际进行路由工作的实体,即负责按照 routing_key 将 message 投递给 queue 。
由 AMQP 协议描述可知,channel 是真实 TCP 连接之上的虚拟连接,所有 AMQP 命令都是通过 channel 发送的,且每一个 channel 有唯一的 ID。一个 channel 只能被单独一个操作系统线程使用,故投递到特定 channel 上的 message 是有顺序的
。
但一个操作系统线程上允许使用多个 channel 。