RabbitMQ详解

3,005 阅读14分钟

RabbitMQ的优点:

  • 开源, 性能有效, 稳定性好
  • 提供可靠性消息投递模式(confirm), 返回模式(return)等
  • 与Spring完美整合, API丰富
  • 集群模式丰富, 支持表达式配置, 高可用HA模式, 镜像队列模型
  • 可以保证数据不丢失的前提下做到高可靠性, 可用性

RabbitMQ高性能原因:

  • 由Erlang语言开发,继承其天生的并发性,稳定性和安全性有保障

RabbitMQ的协议:

AMQP(Advanced Message Queuing Protocol)高级消息队列协议,是一个异步消息传递所使用应用层协议规范,为面向消息中间件设计,基于此协议的客户端与消息中间件可以无视消息来源传递消息,不受客户端、消息中间件、不同的开发语言环境等条件的限制。

设计概念解释:

  • Server : 又称Broker, 接受客户端连接, 实现AMQP实体服务
  • Connection : 连接, 应用程序与Broker的网络连接
  • Channel : 网络信道, 几乎所有的操作都在Channel中进行, Channel是进行消息读写的通道。客户端可以建立多个Channel, 每个Channel代表一个会话任务。
  • Message : 消息, 服务器和应用程序之间传送的数据, 有Properties和Body组成。Properties可以对消息进行修饰, 比如消息的优先级, 延迟等高级特性; Body就是消息体内容。
  • Virtual Host : 虚拟地址, 用于进行逻辑隔离, 最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue, 同一个Virtual Host里面不能有相同名称的Exchange或Queue
  • Exchange : 交换机, 用于接收消息, 根据路由键转发消息到绑定的队列
  • Binding : Exchange和Queue之间的虚拟连接, binding中可以包含routing key
  • Routing Key : 一个路由规则, 虚拟机可用它来确定如何路由一个特定消息
  • Queue : 也成Message Queue, 消息队列, 用于保存消息并将它们转发给消费者

RabbitMQ整体架构

RabbitMQ成员简介

Binding-绑定

  • Exchange和Exchange, Queue之间的连接关系
  • 绑定中可以包含RoutingKey或者参数

Queue-消息队列

  • 消息队列, 实际存储消息数据
  • Durability : 是否持久化
  • Auto delete : 如选yes,代表当最后一个监听被移除之后, 该Queue会自动被删除

Message-消息

  • 服务和应用程序之间传送的数据
  • 本质上就是一段数据, 由Properties和Payload(Body)组成
  • 常用属性 : delivery mode, headers(自定义属性)
  • 其他属性
    • content_type, content_encoding, priority
    • correlation_id : 可以认为是消息的唯一id
    • replay_to : 重回队列设定
    • expiration : 消息过期时间
    • message_id : 消息id
    • timestamp, type, user_id, app_id, cluster_id

Virtual Host-虚拟主机

  • 虚拟地址, 用于进行逻辑隔离, 最上层的消息路由
  • 一个Virtual Host里面可以有若干个Exchange和Queue
  • 同一个Virtual Host里面不能有相同名称的Exchange或Queue

Exchange交换机

接收消息,并根据路由键转发消息到所绑定的队列

注:交换机不会存储消息,如果消息发送到没有绑定消费队列的交换机,消息则丢失。

交换机的属性

  • Name : 交换机名称
  • Type : 交换机类型, direct, topic, fanout, headers
  • Durability : 是否需要持久化, true为持久化
  • Auto Delete : 当最后一个绑定到Exchange上的队列删除后, 自动删除该Exchange
  • Internal : 当前Exchange是否用于RabbitMQ内部使用, 默认为False, 这个属性很少会用到
  • Arguments : 扩展参数, 用于扩展AMQP协议制定化使用

交换机的四种类型

  • Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的

    • 注意 : Direct模式可以使用RabbitMQ自带的Exchange(default Exchange), 所以不需要将Exchange进行任何绑定(binding)操作, 消息传递时, RoutingKey必须完全匹配才会被队列接收, 否则该消息会被抛弃

  • Fanout exchange(扇型交换机)将消息路由给绑定到它身上的所有队列

    • 不处理路由键, 只需要简单的将队列绑定到交换机上

    • 发送到交换机的消息都会被转发到与该交换机绑定的所有队列上

    • Fanout交换机转发消息是最快的

  • Topic exchange(主题交换机)队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列(模糊匹配)
    • “#” : 匹配一个或多个词
    • “*” : 匹配一个词

  • Headers exchange(头交换机)类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。

RabbitMQ常用的5种工作模式

1、点对点(简单)的队列

  • 不需要交换机
  • 一个生产者,一个消费者

2、工作队列(公平性)

  • 不需要交换机
  • 一个生产者,多个消费者,但是一个消息只会发送给一个队列(竞争的消费者模式)
  • 默认是轮询,即会将消息轮流发给多个消费者,但这样对消费得比较慢的消费者不公平
  • 可采用公平分配,即能者多劳
    • channel.basicQos(1);// 限定:发送一条信息给消费者A,消费者A未反馈处理结果之前,不会再次发送信息给消费者A
    • boolean autoAck = false;// 取消自动反馈 channel.basicConsume(QUEUE_NAME, autoAck, consumer);// 接收信息
    • channel.basicAck(envelope.getDeliveryTag(), false);// 反馈消息处理完毕

3、发布/订阅

  • 一个生产者,多个消费者
  • 每一个消费者都有自己的一个队列
  • 生产者没有直接发消息到队列中,而是发送到交换机
  • 每个消费者的队列都绑定到交换机上
  • 消息通过交换机到达每个消费者的队列

该模式就是Fanout Exchange(扇型交换机)将消息路由给绑定到它身上的所有队列

4、路由

生产者发送消息到交换机并指定一个路由key,消费者队列绑定到交换机时要制定路由key(key匹配就能接受消息,key不匹配就不能接受消息)

该模式采用Direct exchange(直连交换机)

5、主题(通配符)

此模式实在路由key模式的基础上,使用了通配符来管理消费者接收消息。生产者P发送消息到交换机X,交换机根据绑定队列的routing key的值进行通配符匹配

符号#:匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor

符号*:只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor

该模式采用Topic exchange(主题交换机)

消息可靠性传递或回退(生产者端)

生产者发送消息出去之后,不知道到底有没有发送到RabbitMQ服务器, 默认是不知道的。而且有的时候我们在发送消息之后,后面的逻辑出问题了,我们不想要发送之前的消息了,需要撤回该怎么做。

AMQP 事务机制

  • txSelect 将当前channel设置为transaction模式
  • txCommit 提交当前事务
  • txRollback 事务回滚

Confirm 模式

消息的确认, 是指生产者投递消息后, 如果Broker收到消息, 则会给我们产生一个应答

生产者进行接收应答, 用来确定这条消息是否正常发送到Broker, 这种方式也是消息的可靠性投递的核心保障

  • 在channel上开启确认模式 : channel.confirmSelect()
  • 在channel上添加监听 : addConfirmListener, 监听成功和失败的返回结果, 根据具体的结果对消息进行重新发送, 或记录日志等后续处理

Return消息机制

Return Listener用于处理一些不可路由的消息

正常情况下消息生产者通过指定一个Exchange和RoutingKey, 把消息送到某一个队列中去, 然后消费者监听队列, 进行消费,但在某些情况下, 如果在发送消息的时候, 当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息, 就要使用Return Listener。

在基础API中有一个关键的配置项Mandatory : 如果为true, 则监听器会接收到路由不可达的消息, 然后进行后续处理(补偿或人工处理), 如果为false, 那么broker端自动删除该消息。

如何保障消息可靠传递

  • 保障消息的成功发出
  • 保障MQ节点的成功接收
  • 发送端收到MQ节点(Broker)的确认应答
  • 完善的消息补偿机制

方案:

1、消息落库, 对消息状态进行标记

  • step1:消息入库
  • step2:消息发送
  • step3:消费端消息确认
  • step4:更新库中消息状态为已确认
  • step5:定时任务读取数据库中未确认的消息
  • step6:未收到确认结果的消息重新发送
  • step7:如果重试几次之后仍然失败, 则将消息状态更改为投递失败的终态, 后面需要人工介入

2、消息的延迟投递, 做二次确认, 回调检查

  • step1 : 第一次消息发送, 必须业务数据落库之后才能进行消息发送
  • step2 : 第二次消息延迟发送, 设定延迟一段时间发送第二次check消息
  • step3 : 消费端监听Broker, 进行消息消费
  • step4 : 消费成功之后, 发送确认消息到确认消息队列
  • step5 : Callback Service监听step4中的确认消息队列, 维护消息状态, 是否消费成功等状态
  • step6 : Callback Service监听step2发送的Delay Check的消息队列, 检测内部的消息状态, 如果消息是发送成功状态, 则流程结束, 如果消息是失败状态, 或者查不到当前消息状态时, 会通知生产者, 进行消息重发, 重新上述步骤

重试机制和幂等性保障(消费者端)

重试机制

消费者在消费消息的时候,如果消费者业务逻辑出现程序异常,会使用消息重试机制。

  • 情况1: 消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试? (需要重试机制)
  • 情况2: 消费者获取到消息后,抛出数据转换异常,是否需要重试?(不需要重试机制)需要发布进行解决。

对于情况2,如果消费者代码抛出异常是需要发布新版本才能解决的问题,那么不需要重试,重试也无济于事。应该采用日志记录+定时任务job健康检查+人工进行补偿

重试机制的实现

在SpringBoot中,@RabbitListener(queue="")用于消费者监听队列。底层使用Aop进行拦截,如果程序没有抛出异常,则自动提交事务。如果抛出异常,该消息会缓存到RabbitMQ服务器,自动实施重试机制,一直到成功为止。可以配置重试间隔时间和重试的次数。

幂等性保障

幂等性:多次执行, 结果保持一致

网络延迟传输中,消费出现异常或者是消费延迟消费,会造成MQ进行重试补偿,在重试过程中,可能会造成重复消费。

解决方案:

  • 唯一ID+指纹码机制

    • 唯一ID + 指纹码机制,利用数据库主键去重
    • SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID +指纹码
    • 好处:实现简单
    • 坏处:高并发下有数据库写入的性能瓶颈
    • 解决方案:跟进ID进行分库分表进行算法路由
  • 利用Redis的原子性去实现

    • 在接收到消息后将消息ID作为key执行 setnx 命令,如果执行成功就表示没有处理过这条消息,可以进行消费了,执行失败表示消息已经被消费了。

自动签收与手动签收(消费端)

默认是自动签收

channel.basicConsume(QUEUE_NAME, false, defaultConsumer);//关闭自动签收,变为手动签收
channel.basicAck(envelope.getDeliveryTag(), false);// 手工签收, 第二个参数表示是否批量签收

消费端限流

消息队列中囤积了大量的消息, 或者某些时刻生产的消息远远大于消费者处理能力的时候, 这个时候如果消费者一次取出大量的消息, 但是客户端又无法处理, 就会出现问题, 甚至可能导致服务崩溃, 所以需要对消费端进行限流

RabbitMQ提供了一种qos(服务质量保证)功能, 即在非自动确认消息的前提下, 如果一定数目的消息(通过consumer或者channel设置qos的值)未被确认前, 不进行消费新的消息

  • 自动签收要设置成false, 建议实际工作中也设置成false
  • void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
    • prefetchSize : 消息大小限制, 一般设置为0, 消费端不做限制
    • prefetchCount : 会告诉RabbitMQ不要同时给一个消费者推送多于N个消息, 即一旦有N个消息还没有ack, 则该consumer将block(阻塞), 直到有消息ack
    • global : true/false 是否将上面设置应用于channel, 简单来说就是上面的限制是channel级别的还是consumer级别 注意 :

prefetchSize和global这两项,RabbitMQ没有实现,暂且不关注,prefetchCount在autoAck设置false的情况下生效,即在自动确认的情况下这个值是不生效的

限流可实现公平队列。

消费端ACK和重回队列

消费端ACK

  • 消费端的手工ACK和NACK, ACK是确认成功消费, NACK表示消息处理失败, 会重发消息
  • 消费端进行消费的时候, 如果由于业务异常我们可以进行日志的记录, 然后进行补偿
  • 如果由于服务器宕机等严重问题, 就需要手工进行ACK保障消费端消费成功

重回队列

  • 消费端重回队列是为了对没有处理成功的消息, 把消息重新回递给Broker
  • 一般在实际应用中, 都会关闭重回队列, 也就是设置为False

TTL队列/消息

  • TTL是Time To Live的缩写, 也就是生存时间
  • RabbitMQ支持消息的过期时间, 在消息发送时可以进行指定
  • RabbitMQ支持队列的过期时间, 从消息入队列开始计算, 只要超过了队列的超时时间配置, 那么消息会自动清除

死信队列(DLX)

  • Dead-Letter-Exchange
  • 利用DLX, 当消息在一个队列中变成死信(dead message)之后, 它能被重新publish到另一个Exchange, 这个Exchange就是DLX
  • DLX也是一个正常的Exchange, 和一般的Exchange没有区别, 它能在任何队列上被指定, 实际上就是设置某个队列的属性为死信队列
  • 当这个队列中有死信时, RabbitMQ就会自动将这个消息重新发布到设置的Exchange上去, 进而被路由到另一个队列
  • 可以监听这个队列中消息做相应的处理, 这个特性可以弥补RabbitMQ3.0以前支持的immediate参数的功能

消息变成死信有以下几种情况 :

  • 消息被拒绝(basic.reject/basic.nack) 并且requeue重回队列设置成false
    • channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); //丢弃消息
  • 消息TTL过期
  • 队列达到最大长度

死信队列的设置 :

  • 首先要设置死信队列的exchange和queue, 然后进行绑定
    • Exchange : dlx.exchange
    • Queue : dlx.queue
    • RoutingKey : #
  • 然后正常声明交换机, 队列, 绑定, 只不过需要在队列加上一个扩展参数即可 : arguments.put(“x-dead-letter-exchange”, “dlx.exchange”);
  • 这样消息在过期, reject或nack(requeue要设置成false), 队列在达到最大长度时, 消息就可以直接路由到死信队列

RabbitMQ集群架构模式

blog.csdn.net/love9056614…

RabbitMQ集群恢复与故障转移

blog.csdn.net/love9056614…

RabbitMQ解决分布式事务思路

RabbitMQ解决分布式事务原理: 采用最终一致性原理。

需要保证以下三要素

  • 确保生产者一定要将数据投递到MQ服务器中
    • 生产者采用confirm,确认应答机制
    • 如果失败,生产者进行重试。
  • MQ消费者消息能够正常消费消息。
    • 采用手动ACK模式,使用补偿机制,注意幂等性问题。
  • 采用补单机制(消息已经投递到MQ中,但是数据回滚了)。
    • 在创建一个补单消费者进行监听,如果订单创建后,又回滚了(写数据库后回滚,数据不一致),此时需要将订单进行补偿。
    • 交换机采用路由键模式,补单队列和派但队列都绑定同一个路由键。

blog.csdn.net/love9056614… blog.csdn.net/love9056614… blog.csdn.net/love9056614… blog.csdn.net/love9056614… www.jianshu.com/p/d8042d7f6… blog.csdn.net/love9056614… blog.csdn.net/love9056614… blog.csdn.net/love9056614…