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 取消超时订单

在这里插入图片描述

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值