RabbitMQ

1. RabbitMQ 基础

简介

RabbitMQ 使用场景:先执行紧要的操作,之后将消息发送到队列,由其他相关服务读取并慢慢执行。

优势:

  • 减少用户等待时间:先执行紧要的操作
  • 流量削峰:支付消息再多也会放到队列中,逐步地被读取
  • 功能解耦,可拓展性强:需要添加功能时,监听队列即可

缺点:

  • 时效性差,不能立即得到调用结果
  • 不确定下游业务执行是否成功
  • 业务安全依赖于消息代理(Broker)的可靠性

RabbitMQ 的整体架构及核心概念:

  • virtual-host:虚拟主机,作用是数据隔离。不同项目使用同一套 RabbitMQ 服务时不会冲突,因为不同 virtual-host 有各自的 exchange 和 queue
  • publisher:消息发送者
  • consumer:消息的消费者
  • queue:队列,存储消息
  • exchange:交换机,负责路由消息

在 RabbitMQ 控制台配置用户和对应的 exchange、queue:

SpringAMQP 如何收发消息?

  • 引入spring-boot-starter-amqp 依赖

  • 配置 rabbitmq 服务端信息

  • 利用 RabbitTemplate 发送消息

  • 利用 @RabbitListener 注解声明要监听的队列,监听消息

Work 模型

让多个消费者绑定到一个队列,共同消费队列中的消息,加快消息处理速度。同一条消息只会被一个消费者处理。

默认情况下,RabbitMQ 会依次轮询,将消息投递给绑定在队列上的每个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。解决方案是:修改 application.yml,设置 preFetch 值为 1。


真正生产环境都会经过exchange来发送消息,而不是直接发送到队列,交换机的类型有三种:Fanout(广播)、Direct(定向)、Topic(话题)

Fanout 交换机

Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的 queue,所以也叫广播模式。

Direct 交换机

Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。

每一个 Queue 都与 Exchange 设置一个 BindingKey,发布者发送消息时,指定消息的 RoutingKey。

Exchange 将消息路由到 BindingKey 与消息 RoutingKey 一致的队列。

Topic 交换机

TopicExchange 与 DirectExchange 类似,区别在于 routingKey 可以是多个单词的列表,并且以.分割。BindingKey 可以使用通配符:# 表示 0 个或多个单词;* 表示 1 个单词

在代码中声明队列和交换机

声明队列和交换机------方式 1

SpringAMQP 提供了几个类,用来声明队列、交换机及其绑定关系:

  • Queue:用于声明队列,也可以用工厂类 QueueBuilder 构建队列
  • Exchange:用于声明交换机,也可以用工厂类 ExchangeBuilder 构建交换机
  • Binding:用于声明队列和交换机的绑定关系,也可以用工厂类 BindingBuilder 构建绑定关系

例如,声明一个 Fanout 类型的交换机,并且创建队列与其绑定:

这种声明和绑定方式有缺点,如 direct 类型的队列指定 routingkey 很繁琐:

声明队列和交换机------方式 2

SpringAMQP 还提供了基于 @RabbitListener 注解来声明队列和交换机的方式:

消息转换器

Spring 的对消息对象的处理是由 org.springframework.amgp.support.converter.MessageConverter 来处理的。而默认实现是 SimpleMessageConverter,基于 JDK 的 ObjectOutputStream 完成序列化。

存在下列问题:JDK 的序列化有安全风险,转换后的消息太长且可读性差。

建议采用 JSON 序列化代替默认的 JDK 序列化,要做两件事情:在 publisher 和 consumer 中都要引入 jackson 依赖:

在 publisher 和 consumer 中(可以在启动类中)都要配置 Messageconverter:

配置好之后,发的什么类型的消息就用什么类型接收:

在项目中使用 RabbitMQ

  • 在 pom 文件中引入 amqp 依赖

  • 在 ymal 文件中配置 mq 地址

  • SpringMVC 自带 jackson 依赖,所以无需引入,直接配置消息转换器

  • 编写监听器(消费者)

  • 编写消息发送方(生产者),也要引入 amqp 依赖并配置 mq 地址

2. RabbitMQ 高级

2.1 消息的可靠性

消息丢失的几种情况:向消息队列中发送消息时出现网络故障、消息队列本身出现故障、交易服务抛出异常

为了保证消息可靠性,就要保证发送者的可靠性、MQ 的可靠性、消费者的可靠性,并用延迟消息作为兜底。

2.1.1 生产者重连

有时,由于网络波动,可能会出现客户端连接 MQ 失败的情况。可以开启连接失败后的重连机制:

当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过 SpringAMQP 提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。

如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。

2.1.2 生产者确认

RabbitMQ 有 Publisher Confirm 和 Publisher Return 两种确认机制。开启确机制认后,在 MQ 成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:

  • 消息投递到了 MQ,但是路由失败。此时会通过 PublisherReturn 返回路由异常原因,然后返回ACK,告知投递成功
  • 临时消息投递到了 MQ,并且入队成功,返回 ACK,告知投递成功
  • 持久消息投递到了 MQ,并且入队完成持久化,返回 ACK,告知投递成功
  • 其它情况都会返回 NACK,告知投递失败

SpringAMQP 实现生产者确认

(1)在生产者的微服务 application.yml 中添加配置:

这里 publisher-confirm-type 有三种模式可选:

  • none:关闭 confirm 机制
  • simple:同步阻塞等待 MQ 的回执消息
  • correlated:异步等待 MQ 的回执消息

(2)编写回调函数:每个 RabbitTemplate 只能配置一个 ReturnCallback,因此需要在项目启动过程中配置

(3)发送消息,指定消息ID、消息 ConfirmCallback(每个消息发送时都单独指定)

如何处理生产者的确认消息?

  • 生产者确认需要额外的网络和系统资源开销,尽量不要使用
  • 如果一定要使用,无需开启 Publisher-Return 机制,因为一般路由失败是自己业务问题
  • 对于 nack 消息可以有限次数重试,依然失败则记录异常消息

如何保证生产者发送消息的可靠性?

首先可以通过配置实现生产者的重连机制,当出现网络波动时尝试重新连接 MQ。

如果其他原因导致失败,可以开启生产者确认机制,当发送消息到 MQ 时,MQ 就会给出回执。若回执是 ACK 则发送成功;若回执是 NACK 则发送失败,此时可以重发消息。通过以上手段,就能基本保证生产者消息的可靠性,但是会增加系统开销。因此,除非对消息可靠性有较高要求,否则基本不采用。

2.2 MQ 的可靠性

在默认情况下,RabbitMQ 会将接收到的信息保存在内存(内存快,降低消息收发的延迟)。这样会导致两个问题:

  • 一旦 MQ 宕机,内存中的消息会丢失
  • 内存空间有限,当消费者故障或处理过慢时,会导致 MQ 中消息积压

保证 MQ 的可靠性有两种方式:数据持久化Lazy Queue,MQ 3.6 之前采用前者,之后有了后者。

2.2.1 数据持久化

RabbitMQ 实现数据持久化包括 3 个方面:

(1)交换机持久化

(2)队列持久化

在 Spring 中创建交换机和队列时会默认创建持久化的交换机和队列。

(3)消息的持久化

如果没有消息持久化,当 MQ 中消息过多时还是会将消息放入磁盘,此时 MQ 是阻塞的,无法处理消息。

2.2.2 Lazy Queue

从 RabbitMQ 的 3.6.0 版本开始,就增加了 Lazy Queue 的概念,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘(内存中只保留最近的消息,默认 2048 条)
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储

在 3.12 版本后,所有队列都是 Lazy Queue 模式,无法更改。

要设置一个队列为惰性队列,只需要在声明队列时,指定 x-queue-mode 属性为 lazy 即可:

RabbitMQ 如何保证消息的可靠性?

  • 首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在;
  • RabbitMQ 在 3.6 版本引入了 LazyQueue,并且在 3.12 版本后成为队列的默认模式。LazyQueue 会将所有消息都持久化;
  • 开启持久化和生产者确认时,RabbitMQ 只有 在消息持久化完成后才会给生产者返回 ACK 回执。

2.3 消费者的可靠性

2.3.1 消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ 提供了消费者确认机制。当消费者处理消息结束后,应该向 RabbitMQ 发送一个回执,告知 RabbitMQ 自己的消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ 从队列中删除该消息
  • nack:消息处理失败,RabbitMQ 需要再次投递消息
  • reject:消息处理失败并拒绝该消息(如:消息格式错误),RabbitMQ 从队列中删除该消息

SpringAMQP 已经实现了消息确认功能。并允许我们通过配置文件选择 ACK 处理方式,有三种方式:

  • none:不处理。即消息投递给消费者后立刻 ack,消息会立刻从 MQ 删除。非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用 api,发送 ack 或 reject,存在业务入侵,但更灵活
  • auto(常用) :自动模式。SpringAMQP 利用 AOP 对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回 ack。当业务出现异常时,根据异常判断返回不同结果:
    • 如果是业务异常,会自动返回 nack
    • 如果是消息处理或校验异常(如:消息格式错误),自动返回 reject

2.3.2 消费失败处理

当消费者出现异常后,消息会不断 requeue(重新入队)到队列,再重新发送给消费者,然后再次异常, 再次 requeue,无限循环,导致 mq 的消息处理飙升,带来不必要的压力。

可以利用 Spring 的 retry 机制,在消费者出现异常时利用本地重试,而不是无限制的 requeue 到 mq 队列:

失败消息处理策略:在开启重试模式后,重试次数耗尽时消息依然失败,则需要有MessageRecoverer 接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接 reject,丢弃消息。默认就是这种方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回 nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(最优

消费者如何保证消息一定被消费?

  • 开启消费者确认机制为 auto,由 spring 确认消息处理成功后返回 ack,异常时返回 nack
  • 开启消费者失败重试机制,并设置 MessageRecoverer 多次重试失败后将消息投递到异常交换机,交由人工处理

2.3.3 业务幂等性

如果一个消息被消费者消费之后,将要返回 ack 时发生了网络波动,那么就可能导致消息重发,消息被消费多次。这显然不合理。

幂等是一个数学概念,用函数表达式来描述是这样的:f(x) = f(f(x))。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。

方案一:唯一消息 id:给每个消息都设置一个唯一 id,利用 id 区分是否是重复消息:

  • 每一条消息都生成一个唯一的 id,与消息一起投递给消费者。
  • 消费者接收到消息后处理自己的业务,业务处理成功后将消息 ID 保存到数据库。
  • 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。

方案二:基于业务判断:结合业务逻辑,基于业务本身做判断。以支付业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理:

如何保证支付服务与交易服务之间的订单状态一致性?

  • 首先,支付服务会正在用户支付成功以后利用 MQ 消息通知交易服务完成订单状态同步。
  • 其次,为了保证 MQ 消息的可靠性,采用了生产者确认机制、消费者确认、消费者失败重试等策略,确保消息投递和处理的可靠性。同时也开启了 MQ 的持久化,避免因服务宕机导致消息丢失。
  • 最后,还在交易服务更新订单状态时做了业务幂等判断,避免因消息重复消费导致订单状态异常。

如果交易服务消息处理失败,有没有什么兜底方案?

可以在交易服务中设置定时任务,定期查询订单支付状态。这样即便 MQ 通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。

2.4 延迟消息

延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。

延时消息有多种实现方案:死信交换机、延迟消息插件

2.4.1 死信交换机

当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):

  • 消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false
  • 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果队列通过 dead-letter-exchange 属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称 DLX)。

死信交换机的方式实现起来比较繁琐。

2.4.2 延迟消息插件

RabbitMQ 的官方也推出了一个插件,支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

发送消息时需要通过消息头 x-delay 来设置延迟时间:

使用延迟消息插件会使服务有一定的性能损耗,因为设置了延迟时间,CPU 要不断计算。这种方案适用于延迟时间较短的情况。

2.4.3 取消超时订单

相关推荐
P.H. Infinity18 分钟前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
WX187021128732 小时前
在分布式光伏电站如何进行电能质量的治理?
分布式
不能再留遗憾了5 小时前
RabbitMQ 高级特性——消息分发
分布式·rabbitmq·ruby
茶馆大橘5 小时前
微服务系列六:分布式事务与seata
分布式·docker·微服务·nacos·seata·springcloud
材料苦逼不会梦到计算机白富美8 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
想进大厂的小王8 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情8 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
许苑向上9 小时前
【零基础小白】 window环境下安装RabbitMQ
rabbitmq
ZHOU西口10 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk10 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka