MQ-初识RabbitMQ和可靠性保障

模型架构

相关概念

Producer生产者:生产者创建消息,然后发布到RabbitMQ中。消息包含两部分:消息体(Payload)和标签(Label)。消息体是一个带有业务逻辑结构的数据。消息标签用来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交给RabbitMQ之后会根据标签把消息发送给感兴趣的消费者。

Consumer消费者:接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。当消费一条消息时,只是消费消息体,在消息路由的过程中,消息的标签会丢弃,存入到队列的只有消息体。

Broker:RabbitMQ的服务节点。

Queue队列:RabbitMQ的内部对象。用于存储消息。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者。

Exchange交换器:生产者将消息发送到交换器,由交换器将消息路由到一个或多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。交换器有四种类型,不同的类型有自己的路由策略。交换器类型:

  1. fanout:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中,无视RoutingKey和BindingKey的绑定规则。
  2. direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中。
  3. topic:与direct类型类似,但是更加灵活:支持用“.”号分隔字符串,支持“*”和“#”用于模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。
  4. headers:不依赖路由键的匹配规则,而是根据发送的消息内容中的 headers(K-V形式) 属性进行匹配。

RoutingKey路由键:生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能生效。

Binding绑定:RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就直到如何正确的将消息路由到队列了。

Connection连接&Channel信道:生产者或消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。这个链接就是一条TCP连接,一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每条信道都会被指派一个唯一ID。信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。RabbitMQ采用类似NIO(Non-blocking I/O)的做法,选择TCP连接服用,以减少性能开销,且便于管理。

AMQP协议:RabbitMQ是遵从AMQP协议的,换句话说RabbitMQ就是AMQP协议的Erlang实现。AMQP协议本身包括三层:

  • Module Layer:位于协议最高层,只要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。
  • Session Layer:位于中间层,只要负责将客户端的命令发送给服务器,再将服务器端的应答返回给客户端,主要为客户端与服务端之间的通信提供可靠性同步机制和错误处理。
  • Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理/信道复用/错误检测和数据表示等。

数据流转流程

生产者核心参数

mandatory:布尔类型

  • true:交换器无法找到符合条件的队列时,RabbitMQ调用Basic.return命令将消息返回给生产者。
  • false:出现上述情形,消息直接丢弃。

此参数需要生产者配合调用channel.addReturnListener来添加ReturnListener监听器来实现。

immediate:布尔类型

true:如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。RabbitMQ调用Basic.return命令将消息返回给生产者。

RabbitMQ3.0版本开始去掉了immediate参数支持,官方解释:immediate参数会影响队列的性能,增加了代码的复杂性,建议采用TTL和DLX的方法代替

Alternate Exchange备份交换器

简称AE,生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失;如果设置了mandatory参数,那么需要添加RetuenListener的编程逻辑,生产者的代码将变得复杂。如果既不想复杂化生产者的代码逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中国呢,在需要的时候去处理这些消息。

可以通过在生命交换器(调用channel.exchangeDeclare方法)的时候添加 alternate-exchange 参数来实现,也可以通过策略(Policy)的方式实现,如果两者同时使用,则前者的优先级更高,会覆盖掉后者。

备份交换器和普通交换器没有太大区别,为了方便使用,建议设置为 fanout 类型,以避免路由键匹配规则带来的不便。

注意事项:

  • 如果设置的备份交换器不存在,客户端和服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有绑定任何队列,
  • 如果备份交换器没有任何匹配的队列,
  • 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效

TTL过期时间

Time to Live的简称,RabbitMQ可以对消息和队列设置TTL。

消息在队列中的生存时间一旦超过TTl值,就会变成“死信”(Dead Message),消费者将无法再收到该消息。

通过在 channel.queueDeclare 方法中加入 x-message-ttl 参数实现消息的TTL设置,单位毫秒。也可以通过策略(Policy)的方式实现。还可以通过调用 HTTP-API 接口设置。

通过 channel.queueDeclare 方法中的 x-expires 参数实现控制队列被自动删除前处于未使用状态的时间,单位毫秒。未使用的意思是队列上没有任何消费者,队列也没有被重新声明,并且在过期时间段内也未调用过 Basic.Get 命令。

注意事项:

  • 如果设置消息TTL为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。这个特性可以部分替代RabbitMQ3.0版本之前的immediate参数,之所以是部分替代,是因为immediate参数在投递失败时会调用Basic.Return将消息返回(这个功能可以使用死信队列实现)。
  • 队列TTL不能设置为0
  • 如果同时设置了消息和队列的TTL,则消息的TTL以两者中较小的那个数值为准。

DLX死信队列

全称 Dead-Letter_Exchange。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到DLX交换器中,绑定DXL交换器的队列称之为死信队列。

通过在 channel.queueDeclare 方法中设置 x-dead-letter-exchange 参数来为这个队列添加DLX。可以为这个DLX添加指定路由键,如果没有添加,则使用原队列的路由键。

消息变成死信一般是由于以下几种情况:

  • 消息被拒绝(Basic.Reject/Basic.Nack),并设置requeue参数为false。
  • 消息过期。
  • 队列达到最大长度。

DLX也是一个正常的交换器,可以监听这个队列中的消息进行响应的处理,与将消息的TTL设置为0配合使用可以弥补immediate参数的功能。

延迟队列

在AMQP协议中或者RabbitMQ本身并没有直接支持延迟队列的功能,但是可以通过DLX和TTL实现延迟队列。

优先级队列

优先级高的队列具有高的优先权,优先级高的消息具备优先被消费的特权。

通过设置队列的 x-max-priority 参数实现。

注意事项:

  • 如果在消费者的消费速度大于生产者的速度,且Broker中没有消息堆积的情况下,对消息设置优先级就没有什么实际意义。

持久化

持久化可以提高RabbitMQ的可靠性,以防在异常情况(重启/关闭/宕机等)下的数据丢失。RabbitMQ的持久化分为三个部分:交换器持久化/队列持久化和消息持久化。

交换器持久化:如果交换器不设置持久化,那么在RabbitMQ服务重启后,交换器的元数据会丢失,不过消息不会丢失,只是生产者不能将效法发送到这个交换器中了。对于一个长期使用的交换器来说,建议将其设置为持久化的。

通过在声明交换器时将 durable 参数设置为true实现。

队列持久化:保证队列本身的元数据不会因异常情况丢失,但是不能保证内部存储的消息不会丢失。

通过在声明队列时将 durable 参数设置为true实现。

消息持久化:通过将消息的投递模式(BasicProperties中的deliveryMode属性)设置为2实现。单单设置队列或消息持久化都不能保证消息不丢失,必须同时设置。

生产者确认

生产者确认机制,是消息正确到达RabbitMQ服务器的保证。RabbitMQ提供两种解决方案:

  1. 事务机制
  2. 发送方确认(publisher confirm)机制

事务机制:

相关方法有三个:

  1. channel.txSelect:将当前信道设置成事务模式
  2. channel.txCommit:用于提交事务
  3. channel.txRollBack:用于回滚事务

注意:事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息,严重降低RabbitMQ的消息吞吐量。

发送方确认(publisher confirm)机制:

生产者通过调用channel.congirmSelect方法(即Confirm.Select命令)将信道设置成 confirm(确认)模式,所有在该信道中发布的消息都会被指派一个唯一ID(从1开始),当消息被成功投递到所有匹配的队列时,RabbitMQ将发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID)。如果消息是持久化的,那么确认的消息会在写入磁盘后发出。此外,也可以设置channel.basicAck方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理。如果RabbitMQ因为自身内部错误导致消息投递失败,会发送一条nack(Basic.Nack)命令给生产者。

confirm机制是异步的,生产者在发送消息之后,在等待信道返回确认的同时继续发送下体条消息。

注意事项:

  • 事务机制和confirm机制是互斥的,同时使用RabbitMQ将会报错。
  • 事务机制和confirm机制是确保消息能够正确发送至RabbitMQ交换器,如果此交换器没有匹配的队列,消息将会丢失。所以需要配合mandatory参数或者备份交换器一起使用来提高消息传输的可靠性。

消费端要点

消息分发

RabbitMQ默认以轮询(round-robin)的分发方式发送消息给消费者,但是如果某些消费者由于某些原因(如机器性能/网络吞吐量等)出现消费能力不同,将造成整体应用的吞吐量下降。此时可以根据消费者消费能力的不同,在消费者订阅消费队列之前,调用 channel.basicQos(int num) 方法,来定制分发策略。

RabbitMQ会保存一个消费者列表,每发送一条消息都对为对应的消费者计数,如果达到了所设定的上限,那么RabbitMQ将不会向这个消费者继续发送消息,直到消费者确认了某条消息之后,RabbitMQ将响应的计数减1,之后消费者可以继续接收消息。

注意事项:

  • Basic.Qos对于拉模式无效

消息顺序性

消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。

消息失序的情况(包括但不限于):

  • 生产者使用事务机制,在发送消息遇到异常进行回滚,重新补发消息,如果消息补偿发送是在另一个线程实现。
  • 生产者使用confirm机制,在发成超时/中断,或者收到RabbitMQ的nack时,同样遇到和事务机制相同的问题。
  • 生产者对发送的消息设置了不同的TTL,并且设置了死信队列,整体上来说是一个延迟队列。
  • 消息设置优先级。
  • 一个队列按顺序发送了m1/m2/m3/m4四个消息,同时这个队列有ConsumerA和ConsumerB两个消费者,ConsumerA收到m1和m3,CunsumerB收到m2和m4,此时CunsumerA收到m1时调用了Basic.Nack/.Reject将消息拒绝,与此同时将requeue设置为true,这样m1由重新进入队列中,之后m1又被发送到了ConsumerB,此时CunsumerB已经消费了m2和m4,之后再消费m1

如果要保证消息的顺序性,需要在业务中做进一步处理,比如在消息体内添加全局有序标识,维护一个有序队列来根据这个标识存储消息。

消息传输保障

一般消息中间件的消息传输保障分为三个层级:

  1. At most once:最多一次。消息可能会丢失,但绝不会重复。
  2. At least once:最少一次。消息不会丢失,但可能会重复。
  3. Exactly once:恰好一次。消息不会丢失,也不会重复。

RabbitMQ支持“最多一次”和“最少一次”。其中“最少一次”需要考虑以下内容:

  1. 生产者开启事务机制或confirm机制,确保消息可靠的传输到RabbitMQ中。
  2. 生产者需要配合使用mandatory参数或者备份交换器,确保消息能够从交换器路由到队列中而不被丢弃。
  3. 消息和队列都进行持久化处理,确保RabbitMQ服务器在遇到异常情况时消息不会丢失。
  4. 消费者在消费消息时开启手动确认(将autoAck设置为false)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章