文章目录
一、RabbitMQ 核心组件
RabbitMQ是基于AMQP协议
开发的一种跨平台、跨语言
的消息队列,有着 削峰、异步、解耦
等优点,其核心组件主要包括RabbitMQ服务端 和 RabbitMQ客户端;其中,服务端包含生产者 (Publisher) 和消费者 (Consumer);
组件 | 作用 |
---|---|
Producer 生产者 | 消息的生产者,向交换机Exchange发布消息的客户端应用程序; |
Virtual Host 虚拟主机 | 虚拟主机,标识一批交换机、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域,其作用就是权限隔离 。vhost是AMQP概念的基础,必须在链接时指定,RabbitMQ默认的vhost是 /; |
Broker 服务节点 | RabbitMQ服务节点,交换机 (Exchange) 与消息队列 (Queue) 都在Broker服务器; |
Exchange 交换机 | 交换机,用来接收生产者(producer)发送的消息,并将这些消息路由给服务器中的队列 (Queue); |
Queue 消息队列 | 消息队列,用来接收交换机(Exchange)发送的消息,消息队列保存消息直到发送给消费者;一个消息可投入一个或多个队列; |
Binding 绑定 | 绑定,用于交换机(Exchange)和消息队列(Queue)之间的关联。一个绑定就是基于路由键 将交换机和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表; |
Channel 信道 | 信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接(一个TCP链接,可以包含多个信道) |
Connection 连接 | 网络连接,比如TCP连接。一个 Consumer需要和Broker建立连接,以获取队列中的消息 |
Message 消息 | 消息,由消息头和消息体组成。消息头包括routing-key(路由键)、priority(优先级)、delivery-mode(消息的路由模式)等属性; |
Consumer 消费者 | 消息的消费者,表示某个监听消息队列的客户端应用程序; |
二、交换机工作模式
模式 | 描述 |
---|---|
Fanout 广播 | 设定 Fanout模式 的交换机,会将消息发送给全部与它绑定的消息队列; |
Direct 直连 | 设定 Direct模式 的交换机,会将消息发送到路由键(Routing Key)与 绑定键(Binding Key)完全匹配的消息队列; |
Topic 主题 | 设定 Topic模式 的交换机,会根据 路由键 与 绑定键 进行模糊匹配 (#,*),将消息发送到能模糊匹配路由规则的消息队列; |
headers 消息头 | 设定 headers模式 的交换机,不依赖于 路由键 与 绑定键 的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配;与Direct模式相似,但性能差,基本不用; |
三、消息的传递过程
首先客户端必须连接到 RabbitMQ 服务器才能发布和消费消息,客户端和 rabbit server 之间会创建一个 tcp 连接,一旦 tcp 打开并通过了认证(认证就是你发送给 rabbit 服务器的用户名和密码),你的客户端和 RabbitMQ 就创建了一条 amqp 信道(channel),信道是创建在“真实” tcp 上的虚拟连接,amqp 命令都是通过信道发送出去的,每个信道都会有一个唯一的 id,不论是发布消息,订阅队列都是通过这个信道完成的
步骤 | 描述 |
---|---|
Step1 | 客户端 (生产者) 生产消息,与Broker服务器建立TCP连接,TCP连接通过认证 (登录) ,则生产者与RabbitMQ会建立一条AMQP信道,信道可以理解为TCP中的一条虚拟连接;AMQP 命令都是通过信道发送出去的,每个信道都会有一个唯一的 id,不论是发布消息,订阅队列都是通过这个信道完成的将消息发送到 交换机 (Exchange); |
Step2 | 生产者生产消息,根据信道发送到MQ的Broker服务器中,然后根据交换机的工作模式 (广播、直连、主题),结合路由键与绑定键,将消息发送给消息队列 (Queue); 注:非广播模式下,那些路由键不匹配的消息,没有队列接收,将会被丢弃; |
Step3 | 消费者订阅RabbitMQ,建立连接后获取信道 (Channel),然后从监听的消息队列 (Queue) 中获取消息进行消费; 注:可以通过以下设置,手动确定消息是消费成功/丢弃/重新放回队列; 1.basic.ack() :确认消息消费成功,Broker将移除此消息;2.basic.reject(deliveryTag,true) :拒绝该消息,第二个参数是true,则重新放回队列;false不放回队列,Broker直接丢弃;3.basic.nack(deliveryTag,true,ture) :批量拒绝监听到的消息,第二个参数指定是否批量拒绝,第三个参数指定是否重新放回消息队列;4.basic.recover(ture) :true,重新放入到消息队列中,并且优先让其他消费者消费;false,则会重新将消息投递给自己; |
四、RabbitMQ消息的持久化机制
1、交换机、队列、消息的持久化
-
重启RabbitMQ后,队列和交换器都会丢失(包括消息),原因在于每个队列和交换器的durable属性均默认为false。RabbitMQ提供了
durable属性来实现持久化
,保证断电后消息不丢失。 -
RabbitMQ 的持久化机制包括为:
交换机
、队列
和消息
持久化持久化 方式 交换机、队列 设置交换机和队列的durable属性为true 消息 持久化消息本身,设置消息的 “投递模式” 属性为2 (delivery_mode = 2) -
因此,RabbitMQ 的消息持久化,需要做到以下三点:
步骤 描述 Step1 将消息的投递模式设置为2 (delivery_mode = 2) Step2 将消息投递到设置了持久化的交换机中 Step3 交换机将消息投递到设置了持久化的消息队列中
2、持久化原理
-
RabbitMQ的持久化原理是:
持久化 1、将持久化的数据写入到磁盘上的一个持久化日志文件中,当数据恢复时,从磁盘读取持久化的数据进行重建; 2、当发布一条经过持久化的消息到持久化的交换机上时,RabbitMQ会等到将消息提交到日志文件后才会响应,对消息做进一步处理; 3、当RabbitMQ重启时,服务器会自动重建交换机与队列,重新将持久化日志中记录的消息发送到合适的交换机或队列上; -
开启消息持久化机制,由于磁盘IO,RabbitMQ服务器的性能与吞吐量会大幅度降低;
五、RabbitMQ事务
RabbitMQ 支持 AMQP 事务,来处理消息丢失的情况(确认机制比事务更轻量
)。AMQP事务与数据库事务不同。
AMQP事务:提供的一种保证消息成功投递的方式,通过将信道开启事务模式后,利用信道 Channel 的三个命令来实现以事务方式发送消息,若发送失败,通过异常处理回滚事务,确保消息成功投递。
- channel.txSelect(): 开启事务
- channel.txCommit() :提交事务
- channel.txRollback() :回滚事务
RabbitMQ的事务非常消耗性能,不但会降低大约2-10倍的消息吞度量,而且会使生产者应用程序之间产生同步,与使用MQ解耦异步系统的初衷相背离。
六、消息确认机制
生产者确认机制—Confirm确认模式
Confirm主要包括两种确认机制;
- confirmCallback(): Broker服务器成功收到生产者的消息后会触发;
- returnCallback(): 交换机投递消息给消息队列,投递失败时会触发
消费者确认机制—ACK机制
步骤 | 核心方法 | 描述 |
---|---|---|
开启Confirm确认模式 | confirm 模式 | 生产者Confirm确认模式,将信道 channel 设置为 Confirm 模式,且只能通过重新创建信道来关闭该设置; |
生产者→Broker服务器 | confirmCallback() | 信道进入Confirm模式,所有经过信道发布的消息均会被指派一个唯一ID。消息一旦被Broker服务器接收到,就会执行confirmCallback()方法;如果是集群模式,则需要所有的Broker服务器均收到消息后,才会执行confirmCallback()方法,通知生产者消息已经成功抵达Broker服务器; |
Exchange交换机→消息队列Queue | returnCallback() | 交换机根据路由键将消息发送到消息队列中,若消息投递失败,则触发returnCallback()方法,通知生产者消息投递失败; |
消息队列→消费者 | ack机制 | 1.basic.ack() :确认消息消费成功,Broker移除此消息;2.basic.reject(deliveryTag,true) :拒绝该消息,第二个参数是true,则重新放回队列;false不放回队列,Broker直接丢弃;3.basic.nack(deliveryTag,true,ture) :批量拒绝监听到的消息,第二个参数指定是否批量拒绝,第三个参数指定是否重新放回消息队列;4.basic.recover(ture) :true,重新放入到消息队列中,并且优先让其他消费者消费;false,则会重新将消息投递给自己; |
七、死信队列的应用
1、项目应用场景
场景:
订单超时未付款,系统应该自动取消订单并且回滚库存;
-
常用解决方案: Spring的@Schedule注解,定时任务轮询数据库查询订单支付状态;
-
缺点: 消耗系统内存,而且增加了数据库的压力,存在较大的时间误差;
解决方案:
利用RabbitMQ中的【消息TTL + 死信交换机】模拟延时队列,从而实现异步通知,解决订单超时与库存回滚;
2、死信概念
-
死信: 是RabbitMQ的一种消息状态,队列中的消息有三种情况会成为死信;
原因 描述 情况1 消息被拒绝 (reject / nack),并且requese = falsle (消息不在重新投递); 情况2 消息的TTL到期; 情况3 消息队列中存储的消息超过最大长度时,队头的消息会变成死信; -
死信交换机: 当一条消息在队列中变成死信后,它会被重新发布到一个交换机中,这个交换机就被称为死信交换机;
-
死信队列: 与死信交换机绑定着的队列就是死信队列,用于处理过期的消息;
3、过期消息
-
RabbitMQ有两种设置消息过期时间的方式:
方式 描述 设置队列本身的过期时间 通过对队列进行设置,队列中所有的消息都存在相同的过期时间,在队列声明的时候使用 x-message-ttl 参数,单位为毫秒;队列剩多少时间过期,那么新来的消息也多长时间过期; 设置消息本身的过期时间
通过对消息本身进行设置,那么每条消息的过期时间都不一样,设置消息属性的 expiration 参数的值,单位为毫秒;
4、延时队列
RabbitMQ中不存在延时队列,但是可以通过设置 消息过期时间(TTL) + 死信交换机 + 死信队列
来模拟实现 延时队列
;
实现延时队列 | 描述 |
---|---|
消息过期 | 设置消息属性的 expiration 参数的值,指定消息过期时间; |
死信队列 | 设置队列参数为 x-dead-letter-exchange ,表明该队列是一个死信队列; |
死信交换机 | 设置交换机参数为 x-dead-letter-exchange,并与死信队列进行绑定,表明该交换机是一个死信交换机; |
消费者监听死信队列,一旦监听到,就证明该消息已经过期了,既可以执行相应的后续逻辑;
比如订单超时后,消息经过死信路由进入死信队列,分别被关闭订单的服务与解锁库存的服务监听到,然后进行关单与解锁库存操作;
八、消息顺序性、消息重复消费、消息丢失、消息积压
问题 | 描述 | 解决方案 |
---|---|---|
消息顺序性问题 | 有些业务场景需要我们保证MQ的顺序性;但是当MQ中一个消息队列被多个消费者监听时,就无法保证消息的顺序性 | 第一步: 将原来的一个queue拆分成多个queue,每个queue都有一个自己的consumer。该种方案的核心是生产者在投递消息的时候根据业务数据关键值(例如订单ID哈希值对订单队列数取模)来将需要保证先后顺序的同一类数据(同一个订单的数据) 发送到同一个queue当中。 第二步: 一个queue对应一个consumer,在consumer中维护多个内存队列,根据业务数据关键值(例如订单ID哈希值对内存队列数取模)将消息加入到不同的内存队列中,然后多个真正负责处理消息的线程去各自对应的内存队列当中获取消息进行消费。 |
消息重复消费问题 | 消息重复性消费 | 关键是保证消息队列的幂等性; ① 给该消息分配一个全局唯一ID,消费成功后插入到数据库中;若出现重复消费的情况,那么由于数据库中已经有了,此时insert操作就会由于主键冲突而报错; ② 引入redis作为消息消费记录存储介质,给消息分配一个全局唯一ID,消息一旦消费成功,就将<id,message>以key-value的形式写入redis中;消费者消费前,先入redis中查询有没有消费记录,若有则不执行; |
消息丢失问题 | 由于MQ宕机或者网络异常等,导致消息丢失的问题 | 消息丢失三种情况:生产者消息丢失 ,MQ消息丢失 ,消费者消息丢失 一、生产者消息丢失——解决方案: 1、基于MQ事务机制: 若消息发送过程中出现异常,则事务会回滚,成功则提交事务;但是这样会大幅降低MQ的吞吐量,不建议开启; 2、基于消息确认机制: 确保生产者的消息能够投递到MQ与指定队列; ① 生产者 → Broker服务器: confirmCallback() ,一旦消息成功投递给Broker,就会回复ACK给生产者;② 生产者 → 消息队列: returnCallback() ,当消息投递给队列失败时触发,若成功投递到消息队列则不触发;二、MQ消息丢失—解决方案: 开启消息持久化机制: 配合消息确认机制confirm使用,首先要把消息持久化到磁盘后,才会向生产者发送ACK;如果MQ宕机了,生产者没有收到ACK,那么生产者会自动重新发送消息; ① 设置交换机和队列的durable属性为true,持久化交换机与消息队列; ② 持久化消息本身,设置消息 “投递模式” 属性为2 (delivery_mode = 2),并将消息投递到持久化的交换机与队列中; 三、消费者消息丢失—解决方案: 启动消息手动确认机制: 如果消费者丢失了消息没有消费,那么MQ会根据 消息重试机制 重新发送消息给消费者,直到消费者回复ACK;basic.ack() 、basic.reject() |
消息积压问题 | 消息积压主要有三种情况: 1、消费者宕机; 2、消费者没有宕机但是由于处理逻辑慢导致消费能力不足,出现积压; 3、生产者单位时间内生产的消息过多,比如双11期间秒杀活动,导致消费者处理不过来; | 一、若业务上能够提前预知可能会出现消息积压问题 ,有以下解决方案:① MQ架构上采用死信的方式:比如订单系统,对业务队列定时30分钟或者定量20万个消息,超过或者超过的部分消息投递给死信队列,监听死信队列的消费者将消息存放到redis或者mysql中,待到数据高峰期过了,再把消息取出来消费处理,细节上如何入库与取出还可以继续优化; ② 业务上采用分布式锁与限流机制,确保生产者不会发送过多的消息; 二、若业务上无法预知是否会出现消息积压问题 ,有以下处理方案:第一步: 首先确定消费者是否宕机,若消费者宕机,那么首先要解决消费者的宕机问题,确保其能恢复消费能力; 第二步: 快速消费积压的消息;大致可以分为两种解决方案: 方案一: 临时增加消费者,提高消费能力,快速消费掉积压的消息; 方案二: 若临时增加消费者,依然不能快速解决消息积压问题,那么可以临时上线一个或者多个服务监听积压的消息队列,快速拿到消息后不做任何逻辑处理,直接将消息存储到redis或者mysql中;待到数据平稳了,再从redis或mysql中取出消息进行处理; 注意: 在这两种方案中,需要考虑【消息丢失问题】与【消息顺序性问题】; ① 对于消息丢失问题:可以设置消息持久化; ② 对于消息积压场景下的消息顺序性问题:需要结合业务实际问题具体分析,比较复杂,用以上发难,会破坏掉消息顺序性; |
九、消息重试机制
在生产环境中,我们一般会设置手动确认消息机制,为了保证消息不丢失且能够消费成功,一般如果出现异常导致消费不成功,我们会通过basicNack()方法不确认消息,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息,通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费…死循环;
所以真实的开发场景一般会有两种方案:
解决方法 | 描述 |
---|---|
方案一 | 设定消息重试次数阈值n;当消费失败后,将 此消息存到redis,并记录消费的次数; 如果消费了n次还是失败,那么就丢弃消息,记录日志存到数据库保存; |
方案二 | 其实很多场景并不是一定要启用消费者应答模式,SpringBoot 给我们提供了一种重试机制,消费者消费时只要捕获到异常 ,则会重试执行消费者业务方法 (不是MQ重发消息); |