首页 > 其他分享 >RabbitMQ 进阶使用之延迟队列 → 订单在30分钟之内未支付则自动取消

RabbitMQ 进阶使用之延迟队列 → 订单在30分钟之内未支付则自动取消

时间:2024-06-03 09:11:44浏览次数:25  
标签:交换器 进阶 队列 30 RabbitMQ 过期 死信 消息

开心一刻

晚上,媳妇和儿子躺在沙发上

儿子疑惑的问道:妈妈,你为什么不去上班

媳妇:妈妈的人生目标是前20年靠父母养,后40年靠你爸爸养,再往后20年就靠你和妹妹养

儿子:我可养不起

媳妇:为什么

儿子:因为,呃...,我和你的想法一样

于家村长_不可置信

讲在前面

如果你们对 RabbitMQ 感到陌生,那可以停止往下阅读了

请先去查阅相关资料,对它有一个基本的了解之后再接着阅读本文

本文会以循序渐进的方式来讲解标题:

使用 RabbitMQ 的延迟队列来实现:订单在30分钟之内未支付则自动取消

所以请你们耐心逐步往下看

另外,实现标题的方式有很多,但本文只讲其中之一的 延迟队列,至于其他方式,不在本文讲解范围之内,如果想了解,烦请你们自行去查阅

消息何去何从

RabbitMQ 的模型架构,相信你们都知道

架构

消息Producer 生成,经 Exchange 路由到 Queue ,然后推给 Consumer 进行消费

消费消息有两种方式

  1. 推模式(Basic.Consume)
  2. 拉模式(Basic.Get)

如果 消息Exchange 无法路由到符合条件的队列时,该 消息 该如何处理,是返还给 Producer 还是直接丢弃?

如果 消息 被路由到 Queue 时发现没有任何消费者,该 消息 该如何处理,是存在 Queue 中还是返还给 Producer ?

作为一个牛皮的中间件,一旦涉及到可选项了,应该怎么做?

我相信你们已经想到了,那肯定是增加配置参数来支持可选项嘛!

mandatory

mandatory 参数用于设置消息是否必须被路由到队列中,默认值是 false

mandatory 参数设置为 true 时,Exchange 无法根据自身的类型和路由键找到一个符合条件的 Queue,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃

mandatory 值为 false

mandaroty_false

代码执行正常,但没有输出结果,所以我们不确定消息是否投递了

但我们可以通过 RabbitMQ 管理界面,看 Exchange 概况

mandaroty_exchange

来确定消息确实投递了

mandatory 值为 true 时,需要添加一个监听器 ReturnListener

mandatory_true

代码执行正常,同时也有输出结果

2024-06-01 14:54:52|AMQP Connection 10.5.108.226:5672|com.qsl.rabbit.PriorityMessageTest|INFO|59|Basic.Return 返回结果:mandatory test

也可以通过 RabbitMQ 管理界面,看 Exchange 概况来确定消息是否投递过

作为拓展,给你们留两个问题

  1. mandatory 设置为 true 的同时,不添加监听器 ReturnListener,会是什么结果
  2. mandatory 设置为 false 的同时,添加监听器 ReturnListener,又会是什么结果

immediate

immediate 参数用于设置消息是否立即发送给消费者,默认值是 false

immediate 参数设置为 true 时,如果消息路由到队列时发现队列上并没有任何消费者,那么该消息不会存入队列中,当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者

immediate 为 true ,消息路由到匹配的队列时

  1. 部分队列有消费者,有消费者的队列会立即将消息投递给消费者,没有消费者的队列会丢弃该消息
  2. 全部队列都没有消费者,则将该消息返回给生产者

执行如下代码

immediate_true

你会发现报错

2024-06-01 16:16:06|AMQP Connection 10.5.108.226:5672|org.springframework.amqp.rabbit.connection.CachingConnectionFactory|ERROR|1575|Channel shutdown: connection error; protocol method: #method<connection.close>(reply-code=540, reply-text=NOT_IMPLEMENTED - immediate=true, class-id=60, method-id=40)

这是因为从 RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持,对此官方解释如下

immediate 参数会影响镜像队列的性能,增加了代码复杂性,建议采用 TTLDLX 替代 immediate

概括来讲,mandatory 针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者。immediate 针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者

Alternate Exchange

生产者在发送消息时,如果不设置 mandatory 参数(或设置为 false),那么消息在未被路由的情况下会丢失;如果设置了 mandatory(且设置成 true),那么需要添加对应的 ReturnListener 逻辑,生产者的代码会变得复杂。如果既不想增加生产者的复杂,又不想消息丢失,那么就可以使用备份交换器(Alternate Exchange),将未被路由的消息存储在 RabbitMQ 中,在需要的时候再去处理这些消息

实现代码如下

备份交换器实现

执行如下测试代码

alternate_测试代码

消息通过 com.qsl.normal.exchange ,经路由键 123 未匹配到任何队列,此时消息就会发送给 com.qsl.normal.exchange 的备份交换器 com.qsl.alternate.exchange,因为备份交换器的类型是 fanout,所以消息会被路由到 com.qsl.alternate.exchange 绑定的所有队列上,目前只有一个队列 com.qsl.unrouted.queue ,所以消息最终来到 com.qsl.unrouted.queue,消息流转如下

备用交换器

RabbitMQ 控制台看队列状况如下

alternate_测试结果

备份交换器和普通的交换器没有太大的区别,为了方便使用,推荐选择 fanout 类型;你们也可以选择其他类型,比如 directtopic,但此时需要保证消息被重新路由到备份交换器的路由键和生产者发出的路由键是一样的,否则消息不能正确路由到备份交换器的队列中,消息会丢失!

关于备份交换器,以下几种特殊情况需要注意

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

过期时长(TTL)

TTL,Time to Live 的简称,字面意思生存时长,也有很多人称过期时间,个人更习惯称过期时长

消息的 TTL

RabbitMQ 有两种方法对消息设置过期时长

  1. 通过队列属性设置,队列中的所有消息都有相同的过期时长
  2. 对消息本身进行单独设置,每条消息的过期时长可以不同

如果两种方法一起使用,则消息的过期时长以两者之间较小值为准(而非单纯的以消息的过期时长为准)

消息在队列中的生存时间一旦超过设置的过期时长,就会变成 死信(Dead Message) ,消费者将无法再通过正常的路由收到该消息

可以通过绑定 死信队列 来消费 Dead Message

通过队列属性 x-message-ttl 可以设置消息的过期时长,单位是毫秒,示例代码如下

消息ttl队列

如果不设置 TTL,消息不会过期;如果 TTL 设置成 0,则表示除非此时可以将消息直接投递给消费者,否则该消息直接被丢弃,这个特性是不是看起来很眼熟?回过头去看看 immediatetrue 时的第 1 个特性

1.部分队列有消费者,有消费者的队列会立即将消息投递给消费者,没有消费者的队列会丢弃该消息

通过参数 expiration 可以单独设置每个消息的过期时长,单位也是毫秒,示例代码如下

消息ttl

这两种方法的过期策略是怎样的,大家思考下再往下看

对于设置队列属性 x-message-ttl 的方法,队列中的消息具有相同的过期时长,队列中已过期的消息肯定是在队列头部,RabbitMQ 只需要定期的从队头开始往队尾扫描,一旦消息过期则从队列中剔除,一旦扫描到 未过期 的消息,则本次扫描完成

对于设置参数 expiration 的方法,每个消息可以设置不同的过期时长,那么过期的消息不一定在队列头部,如果要删除队列中所有过期消息,只能扫描整个队列,此时的成本是比较高的,所以采用惰性删除,即消息即将被投递给消费者时做过期判定,如果过期则进行删除

如果既设置了队列属性 x-message-ttl,又设置了 expiration,那该如何判定消息是否过期了呢?

定期删除 + 惰性删除,Redis 的过期策略是不是也是这个?

队列的 TTL

这里针对的是队列,而非队列中的消息,大家别和 消息的 TTL 搞混了

通过参数 x-expires 可以设置队列被自动删除前处于未使用状态的时长,单位是毫秒,不能设置为 0

未使用状态需要满足三点

  1. 队列上没有任何消费者
  2. 队列也没有被重新声明
  3. 过期时间段内未调用过 Basic.Get 命令

RabbitMQ 能保证在过期时长到达后将队列删除,但不保障及时。RabbitMQ 重启后,持久化的队列的过期时长会被重新计算

如下是创建一个过期时长为 30 分钟的队列

ttl队列

队列信息如下

ttl队列 rabbitmq控制台

死信队列

死信队列 之前,我们得先了解 DLX,全称 Dead-Letter-Exchange,中文翻译成 死信交换器

当消息在一个队列中变成死信之后

消息变成死信的情况包括以下3种

  1. 消息被决绝(Basic.Reject/Basic.Nack),并设置参数 requeuefalse
  2. 消息过期
  3. 队列达到最大长度

它能被重新发送到另一个交换器中,这个交换器就是 DLX,而绑定到 DLX 的队列就是 死信队列

DLX 也是一个正常的交换器,和一般的交换器没有区别,它可以和任何队列进行绑定,当绑定的队列中存在死信时,RabbitMQ 就会自动将这个消息重新发布到设置的 DLX 上,进而被路由到 死信队列死信队列 也是可以被监听的,也可以有消费者对 死信队列 中的消息进行消费处理的

所以,死信队列 可以变相的实现 immediatetrue 时的第 2 种情况

2.全部队列都没有消费者,则将该消息返回给生产者

为什么是 变相,因为不是直接将消息返回给生产者,而是生产者可以监听 死信队列 ,使消息回到生产者;虽然结果一致,但实现方式还是有区别的

那么 immediatetrue 的特性,就可以用 TTL + 死信队列 来替代了

通过参数 x-dead-letter-exchange 可以给队列添加 DLX;通过参数 x-dead-letter-routing-key 可以给这个 DLX 指定路由键,如果未配置该参数,则使用原队列的路由键,实现代码如下

DLX实现

执行如下测试代码

DLX_Test

消息通过交换器 com.qsl.normal.exchange,经路由键 ttlMessage 匹配到队列 com.qsl.message.ttl.queue 中,队列设置了 x-message-ttl 为 3000 毫秒,这段时长内队列上没有消费者消费这条消息,消息过期。由于给队列设置了死信交换器 com.qsl.dlx.exchange,消息会通过该交换器,经路由键 dlx_routing_key 匹配到队列 com.qsl.dlx.queue 中,消息最终存储在该死信队列中,消息流转如下

RabbitMQ 进阶-死信队列

RabbitMQ 控制台,可以看到队列状况如下

死信队列状况

DLX 是一个非常有用的特性,它可以处理异常情况下,消息不能够被消费者正确消费而被置入到死信队列中,保证消息不被丢失;后续分析程序可以通过消费死信队列中的消息来分析当时所遇到的异常情况,进而改善和优化系统

DLX 还有一个很重要的功能,它配合 TTL 可以实现延迟队列,具体实现请往下看

延迟队列

延迟队列存储的对象是延迟消息

延迟消息 指的是需要延迟消费的消息

就是当消息发送之后,并不想让消费者立即拿到消息,而是等待特定时长后,消费者才拿到消息进行消费

延迟队列的使用场景有很多,例如:

  1. 订单系统中,下单完成之后 30 分钟内完成支付,否则取消订单
  2. 用户注册成功后,如果三天内没有登陆则进行短信提醒
  3. 远程控制扫地机器人,2 个小时后进行房间打扫
  4. ...

RabbitMQ 本身并没有直接支持 延迟队列 的功能,但是可以通过 DLXTTL 模拟出 延迟队列 的功能,具体实现已经在上一节(死信队列)中完成了,你们可以网上翻一翻

给大家演示 场景1 的完整示例,时间改成 1 分钟内完成支付

生产者端配置

order 配置

消费者端配置

order 消费者

消息发送

order 消息发送

输出日志如下

order 日志

实际应用中,可以根据延迟时长给延迟队列划分多个等级,例如

RabbitMQ 进阶-多维度死信队列

目前 RabbitMQ 提供了另外的方式来实现 延迟队列

https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

感兴趣的可以去看看

总结

  1. 示例代码:spring-boot-rabbitmq

  2. mandatory 与 immediate

    mandatory 针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者

    immediate 针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者

    RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持,可以用 DLXTTL 来代替

  3. 过期时长

    消息的过期时长有两种设置方式:队列的参数 x-message-ttl 和消息的参数 expiration

    队列也可以设置过期时长,该时长内队列一直处于未使用状态则会被删除;通过队列参数 x-expires 来设置

  4. 死信队列

    绑定到死信交换器(DLX)上的队列就是死信队列

    DLX 能够保证异常的情况下消息不会丢失,后续通过分析死信队列中的消息,可以改善和优化系统

  5. 延迟队列

    目前来讲,实现延迟队列的方式有两种

    1. DLXTTL
    2. rabbitmq-delayed-message-exchange

参考

《RabbitMQ实战指南》

标签:交换器,进阶,队列,30,RabbitMQ,过期,死信,消息
From: https://www.cnblogs.com/youzhibing/p/18226063

相关文章

  • RabbitMQ的详解和使用
    一、什么是MQ?1、MQ的概念MQ全称MessageQueue(消息队列),是在消息的传输过程中保存消息的容器。多用于系统之间的异步通信。下面用图来理解异步通信,并阐明与同步通信的区别。同步通信:甲乙两人面对面交流,你一句我一句必须同步进行,两人除此之外不做任何事情 异步通信:异步通信......
  • 【C语言进阶】--- 动态内存管理
    动态内存管理函数1.malloc函数void*malloc(size_tsize);功能:向堆区的空间中申请一块大小为size个字节的空间,返回指向这块空间的指针如果开辟失败会返回一个NULL指针,因此要检查malloc的返回值,避免返回NULL指针后再访问空指针malloc申请的空间,程序退出后会还给操作系统......
  • 代码随想录算法训练营第二十一天 | 530.二叉搜索树的最小绝对差 501.二叉搜索树中的众
    530.二叉搜索树的最小绝对差题目链接文章讲解视频讲解关键词:二叉搜索树-->中序遍历关于递归的返回值  由于需要遍历整棵二叉树,所以返回值为void,如果不是遍历整棵二叉树,需要在得到结果时立即返回结果,此时返回值才不为空怎样使用两个指针pre和cur使得pre始终指向cur的前......
  • MySQL进阶之索引
    1索引概述  索引(index)是帮助MySOL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。  索引的优缺点优势劣势......
  • [目标检测数据集]变电站缺陷检测数据集8307张17类别VOC和YOLO格式
    数据集格式:PascalVOC格式+YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件)图片数量(jpg文件个数):8307标注数量(xml文件个数):8307标注数量(txt文件个数):8307标注类别数:17标注类别名称:[“bj_bpmh”,“bj_bpps”,“bj_wkps”,......
  • 【JavaEE 进阶(二)】Spring MVC(下)
    ❣博主主页:33的博客❣▶️文章专栏分类:JavaEE◀️......
  • 【云原生进阶之数据库技术】第二章-Oracle-使用-3.3.2-Oracle Data Guard原理
    2DataGuard原理解析2.1数据同步原理        DG的核心组件包括:主数据库:负责处理所有的写操作,并将这些操作记录在重做日志(RedoLogs)中。备用数据库:可以是物理备用数据库(PhysicalStandby)或逻辑备用数据库(LogicalStandby)。物理备用数据库通常是只读的,而逻辑备用......
  • 为什么GD32F303代码运行在flash比sram更快?
    我们知道一般MCU的flash有等待周期,随主频提升需要插入flash读取的等待周期,以stm32f103为例,主频在72M时需要插入2个等待周期,故而代码效率无法达到最大时钟频率。所以STM32F103将代码加载到sram运行速度更快。但使用GD32F303时将代码加载到SRAM后速度反而下降了一些,这是为什么......
  • 【GD32F303红枫派使用手册】第六节 PMU-低功耗实验
    6.1实验内容通过本实验主要学习以下内容:PMU原理;低功耗的进入以及退出操作;6.2实验原理6.2.1PMU结构原理PMU即电源管理单元,其内部结构下图所示,由该图可知,GD32F303系列MCU具有三个电源域,包括VDD/VDDA电源域、1.2V电源域以及电池备份域,其中,VDD/VDDA域由电源直接供电。在......
  • 202305青少年软件编程(Python)等级考试试卷(四级)
    第1题【单选题】有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。问第n年的时候,共有多少头母牛?由递推法可推测,当年数小于等于4的时候,第几年就是有几头牛,即a[1]=1;a[2]=2;a[3]=3;a[4]=4。当n大于4的时候,这时候第......