RabbitMQ 进阶使用之延迟队列 → 订单在30分钟之内未支付则自动取消
开心一刻
晚上,媳妇和儿子躺在沙发上
儿子疑惑的问道:妈妈,你为什么不去上班
媳妇:妈妈的人生目标是前20年靠父母养,后40年靠你爸爸养,再往后20年就靠你和妹妹养
儿子:我可养不起
媳妇:为什么
儿子:因为,呃...,我和你的想法一样

讲在前面
如果你们对 RabbitMQ 感到陌生,那可以停止往下阅读了
请先去查阅相关资料,对它有一个基本的了解之后再接着阅读本文
本文会以循序渐进的方式来讲解标题:
使用 RabbitMQ 的延迟队列来实现:订单在30分钟之内未支付则自动取消
所以请你们耐心逐步往下看
另外,实现标题的方式有很多,但本文只讲其中之一的 延迟队列,至于其他方式,不在本文讲解范围之内,如果想了解,烦请你们自行去查阅
消息何去何从
RabbitMQ 的模型架构,相信你们都知道

消息 由 Producer 生成,经 Exchange 路由到 Queue ,然后推给 Consumer 进行消费
消费消息有两种方式
- 推模式(Basic.Consume)
- 拉模式(Basic.Get)
如果 消息 经 Exchange 无法路由到符合条件的队列时,该 消息 该如何处理,是返还给 Producer 还是直接丢弃?
如果 消息 被路由到 Queue 时发现没有任何消费者,该 消息 该如何处理,是存在 Queue 中还是返还给 Producer ?
作为一个牛皮的中间件,一旦涉及到可选项了,应该怎么做?
我相信你们已经想到了,那肯定是增加配置参数来支持可选项嘛!
mandatory
mandatory 参数用于设置消息是否必须被路由到队列中,默认值是 false
当 mandatory 参数设置为 true 时,Exchange 无法根据自身的类型和路由键找到一个符合条件的 Queue,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃
当 mandatory 值为 false 时

代码执行正常,但没有输出结果,所以我们不确定消息是否投递了
但我们可以通过 RabbitMQ 管理界面,看 Exchange 概况

来确定消息确实投递了
当 mandatory 值为 true 时,需要添加一个监听器 ReturnListener

代码执行正常,同时也有输出结果
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 概况来确定消息是否投递过
作为拓展,给你们留两个问题
- mandatory 设置为 true 的同时,不添加监听器 ReturnListener,会是什么结果
- mandatory 设置为 false 的同时,添加监听器 ReturnListener,又会是什么结果
immediate
immediate 参数用于设置消息是否立即发送给消费者,默认值是 false
当 immediate 参数设置为 true 时,如果消息路由到队列时发现队列上并没有任何消费者,那么该消息不会存入队列中,当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者
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参数会影响镜像队列的性能,增加了代码复杂性,建议采用TTL和DLX替代immediate
概括来讲,mandatory 针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者。immediate 针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者
Alternate Exchange
生产者在发送消息时,如果不设置 mandatory 参数(或设置为 false),那么消息在未被路由的情况下会丢失;如果设置了 mandatory(且设置成 true),那么需要添加对应的 ReturnListener 逻辑,生产者的代码会变得复杂。如果既不想增加生产者的复杂,又不想消息丢失,那么就可以使用备份交换器(Alternate Exchange),将未被路由的消息存储在 RabbitMQ 中,在需要的时候再去处理这些消息
实现代码如下

执行如下测试代码

消息通过 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 控制台看队列状况如下

备份交换器和普通的交换器没有太大的区别,为了方便使用,推荐选择 fanout 类型;你们也可以选择其他类型,比如 direct 或 topic,但此时需要保证消息被重新路由到备份交换器的路由键和生产者发出的路由键是一样的,否则消息不能正确路由到备份交换器的队列中,消息会丢失!
关于备份交换器,以下几种特殊情况需要注意
- 如果设置的备份交换器不存在,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
- 如果备份交换器没有绑定任何队列,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
- 如果备份交换器没有任何匹配的队列,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
- 如果备份交换器和 mandatory 参数一起使用,mandatory 会失效
过期时长(TTL)
TTL,Time to Live 的简称,字面意思生存时长,也有很多人称过期时间,个人更习惯称过期时长
消息的 TTL
RabbitMQ 有两种方法对消息设置过期时长
- 通过队列属性设置,队列中的所有消息都有相同的过期时长
- 对消息本身进行单独设置,每条消息的过期时长可以不同
如果两种方法一起使用,则消息的过期时长以两者之间较小值为准(而非单纯的以消息的过期时长为准)
消息在队列中的生存时间一旦超过设置的过期时长,就会变成 死信(Dead Message) ,消费者将无法再通过正常的路由收到该消息
可以通过绑定
死信队列来消费Dead Message
通过队列属性 x-message-ttl 可以设置消息的过期时长,单位是毫秒,示例代码如下

如果不设置 TTL,消息不会过期;如果 TTL 设置成 0,则表示除非此时可以将消息直接投递给消费者,否则该消息直接被丢弃,这个特性是不是看起来很眼熟?回过头去看看 immediate 为 true 时的第 1 个特性
1.部分队列有消费者,有消费者的队列会立即将消息投递给消费者,没有消费者的队列会丢弃该消息
通过参数 expiration 可以单独设置每个消息的过期时长,单位也是毫秒,示例代码如下

这两种方法的过期策略是怎样的,大家思考下再往下看
对于设置队列属性 x-message-ttl 的方法,队列中的消息具有相同的过期时长,队列中已过期的消息肯定是在队列头部,RabbitMQ 只需要定期的从队头开始往队尾扫描,一旦消息过期则从队列中剔除,一旦扫描到 未过期 的消息,则本次扫描完成
对于设置参数 expiration 的方法,每个消息可以设置不同的过期时长,那么过期的消息不一定在队列头部,如果要删除队列中所有过期消息,只能扫描整个队列,此时的成本是比较高的,所以采用惰性删除,即消息即将被投递给消费者时做过期判定,如果过期则进行删除
如果既设置了队列属性 x-message-ttl,又设置了 expiration,那该如何判定消息是否过期了呢?
定期删除 + 惰性删除,
Redis的过期策略是不是也是这个?
队列的 TTL
这里针对的是队列,而非队列中的消息,大家别和 消息的 TTL 搞混了
通过参数 x-expires 可以设置队列被自动删除前处于未使用状态的时长,单位是毫秒,不能设置为 0
未使用状态需要满足三点
- 队列上没有任何消费者
- 队列也没有被重新声明
- 过期时间段内未调用过
Basic.Get命令
RabbitMQ 能保证在过期时长到达后将队列删除,但不保障及时。RabbitMQ 重启后,持久化的队列的过期时长会被重新计算
如下是创建一个过期时长为 30 分钟的队列

队列信息如下

死信队列
讲 死信队列 之前,我们得先了解 DLX,全称 Dead-Letter-Exchange,中文翻译成 死信交换器
当消息在一个队列中变成死信之后
消息变成死信的情况包括以下3种
- 消息被决绝(
Basic.Reject/Basic.Nack),并设置参数requeue为false- 消息过期
- 队列达到最大长度
它能被重新发送到另一个交换器中,这个交换器就是 DLX,而绑定到 DLX 的队列就是 死信队列
DLX 也是一个正常的交换器,和一般的交换器没有区别,它可以和任何队列进行绑定,当绑定的队列中存在死信时,RabbitMQ 就会自动将这个消息重新发布到设置的 DLX 上,进而被路由到 死信队列。死信队列 也是可以被监听的,也可以有消费者对 死信队列 中的消息进行消费处理的
所以,死信队列 可以变相的实现 immediate 为 true 时的第 2 种情况
2.全部队列都没有消费者,则将该消息返回给生产者
为什么是 变相,因为不是直接将消息返回给生产者,而是生产者可以监听 死信队列 ,使消息回到生产者;虽然结果一致,但实现方式还是有区别的
那么 immediate 为 true 的特性,就可以用 TTL + 死信队列 来替代了
通过参数 x-dead-letter-exchange 可以给队列添加 DLX;通过参数 x-dead-letter-routing-key 可以给这个 DLX 指定路由键,如果未配置该参数,则使用原队列的路由键,实现代码如下

执行如下测试代码

消息通过交换器 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 控制台,可以看到队列状况如下

DLX 是一个非常有用的特性,它可以处理异常情况下,消息不能够被消费者正确消费而被置入到死信队列中,保证消息不被丢失;后续分析程序可以通过消费死信队列中的消息来分析当时所遇到的异常情况,进而改善和优化系统
DLX 还有一个很重要的功能,它配合 TTL 可以实现延迟队列,具体实现请往下看
延迟队列
延迟队列存储的对象是延迟消息
延迟消息指的是需要延迟消费的消息就是当消息发送之后,并不想让消费者立即拿到消息,而是等待特定时长后,消费者才拿到消息进行消费
延迟队列的使用场景有很多,例如:
- 订单系统中,下单完成之后 30 分钟内完成支付,否则取消订单
- 用户注册成功后,如果三天内没有登陆则进行短信提醒
- 远程控制扫地机器人,2 个小时后进行房间打扫
- ...
RabbitMQ 本身并没有直接支持 延迟队列 的功能,但是可以通过 DLX 和 TTL 模拟出 延迟队列 的功能,具体实现已经在上一节(死信队列)中完成了,你们可以网上翻一翻
给大家演示 场景1 的完整示例,时间改成 1 分钟内完成支付
生产者端配置

消费者端配置

消息发送

输出日志如下

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

目前 RabbitMQ 提供了另外的方式来实现 延迟队列
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
感兴趣的可以去看看
总结
示例代码:spring-boot-rabbitmq
mandatory 与 immediate
mandatory针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者immediate针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者RabbitMQ 3.0版本开始去掉了对immediate参数的支持,可以用DLX和TTL来代替过期时长
消息的过期时长有两种设置方式:队列的参数
x-message-ttl和消息的参数expiration队列也可以设置过期时长,该时长内队列一直处于未使用状态则会被删除;通过队列参数
x-expires来设置死信队列
绑定到死信交换器(
DLX)上的队列就是死信队列DLX能够保证异常的情况下消息不会丢失,后续通过分析死信队列中的消息,可以改善和优化系统延迟队列
目前来讲,实现延迟队列的方式有两种
DLX与TTL- rabbitmq-delayed-message-exchange
参考
《RabbitMQ实战指南》
RabbitMQ 进阶使用之延迟队列 → 订单在30分钟之内未支付则自动取消的更多相关文章
- Java 实现订单未支付超时自动取消
在电商上购买商品后,如果在下单而又没有支付的情况下,一般提示30分钟完成支付,否则订单自动.比如在京东下单为完成支付: 超过24小时,就会自动取消订单,下面使用 Java 定时器实现超时取消订单功能. ...
- RabbitMQ 3.6.12延迟队列简单示例
简介 延迟队列存储的消息是不希望被消费者立刻拿到的,而是等待特定时间后,消费者才能拿到这个消息进行消费.使用场景比较多,例如订单限时30分钟内支付,否则取消,再如分布式环境中每隔一段时间重复执行某操作 ...
- 基于rabbitMQ 消息延时队列方案 模拟电商超时未支付订单处理场景
前言 传统处理超时订单 采取定时任务轮训数据库订单,并且批量处理.其弊端也是显而易见的:对服务器.数据库性会有很大的要求,并且当处理大量订单起来会很力不从心,而且实时性也不是特别好 当然传统的手法还可 ...
- 【RabbitMQ】一文带你搞定RabbitMQ延迟队列
本文口味:鱼香肉丝 预计阅读:10分钟 一.说明 在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列.相信通过上一篇的学习,对于死信队列已经有了更 ...
- .Net之延迟队列
介绍 具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费. 使用场景 延时队列在项目中的应用还是比较多的,尤其像电商类平台: 订单成功后,在30分钟内 ...
- 【RabbitMQ 实战指南】一 延迟队列
1.什么是延迟队列 延迟队列中存储延迟消息,延迟消息是指当消息被发送到队列中不会立即消费,而是等待一段时间后再消费该消息. 延迟队列很多应用场景,一个典型的应用场景是订单未支付超时取消,用户下单之后3 ...
- rabbitmq 实现延迟队列的两种方式
原文地址:https://blog.csdn.net/u014308482/article/details/53036770 ps: 文章里面延迟队列=延时队列 什么是延迟队列 延迟队列存储的对象肯定 ...
- RabbitMQ如何实现延迟队列?(转)
什么是延迟队列 延迟队列存储的对象肯定是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费. 场景一 ...
- Spring Boot(十四)RabbitMQ延迟队列
一.前言 延迟队列的使用场景:1.未按时支付的订单,30分钟过期之后取消订单:2.给活跃度比较低的用户间隔N天之后推送消息,提高活跃度:3.过1分钟给新注册会员的用户,发送注册邮件等. 实现延迟队列的 ...
- RabbitMQ 延迟队列,消息延迟推送
目录 应用场景 消息延迟推送的实现 测试结果 应用场景 目前常见的应用软件都有消息的延迟推送的影子,应用也极为广泛,例如: 淘宝七天自动确认收货.在我们签收商品后,物流系统会在七天后延时发送一个消息给 ...
随机推荐
- 【Windbg Preview】Failed to load data access DLL, 0x80004005
最近使用Windbg的时候一直在用Preview版本,感觉解析一下就能直接加载起环境来太爽了.不过最近遇到一个dump加载不起来了. 但是最近一次加载却失败了,尝试了很久也不行 Failed to l ...
- 深度解读《深度探索C++对象模型》之拷贝构造函数
接下来我将持续更新"深度解读<深度探索C++对象模型>"系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文. 写作不易,请有心人到我的公众号上 ...
- 国内首家!百度智能云宣布支持Llama3全系列训练推理
继18日Llama3的8B.70B大模型发布后,百度智能云千帆大模型平台19日宣布在国内首家推出针对Llama3全系列版本的训练推理方案,便于开发者进行再训练,搭建专属大模型,现已开放邀约测试. 目前 ...
- Python环境和PyCharm搭建教程
1.python下载和安装 1.访问Python 官网:https://www.python.org/ 2.以Windows为例,我们选择一个稳定的版本进行安装,这里需要注意选择和自己操作系统类型一致 ...
- 力扣176(MySQL)-第二高的薪水(中等)
题目: id 是这个表的主键.表的每一行包含员工的工资信息. 编写一个 SQL 查询,获取并返回 Employee 表中第二高的薪水 .如果不存在第二高的薪水,查询应该返回 null . 查询结果如下 ...
- CF1481D AB Graph 题解
CF1481D AB Graph 题解 [思路] 首先有几个显而易见的东西. 如果存在两个点,他们之间的两条边字母相同,那么一定有解(在两个点之间跳.) 否则,这张图的邻接矩阵一定长成这样: * a ...
- Sentinel 1.7.2 发布,完善开源生态及扩展性
多样化的适配模块 到目前为止,Sentinel 已覆盖微服务.API Gateway 和 Service Mesh 三大板块的核心生态,同时多语言已推出 Java.C++.Go 三种语言的原生实现. ...
- 针对数据库连接池到DRDS连接探活的优化
简介: 针对数据库连接池到DRDS连接探活的优化 1. 问题背景 近期在给某专有云客户进⾏云产品应⽤性能优化分析时,发现了⼀个有趣的关于DRDS使⽤层⾯的问题,这⾥给⼤家分享⼀下.使⽤过DRDS产品的 ...
- 阿里巴巴云原生混部系统 Koordinator 正式开源
简介: 脱胎于阿里巴巴内部,经过多年双 11 打磨,每年为公司节省数十亿的混部系统 Koordinator 今天宣布正式开源.通过开源,我们希望将更好的混部能力.调度能力开放到整个行业,帮助企业客户 ...
- 云效Codeup代码评审中的代码协同
简介: 云效 Codeup 汇集了阿里巴巴最新的代码托管.代码协同技术,希望能够造福更多中国和世界的开发者. 大神说:"Show me the code",于是就有了代码评审. & ...